iotsaas/docs/api/doc/panel.html
2995840376 0ce191c448 chore: 初始化 iotsaas 版本库基线
纳入范围:
- backend/  Spring Boot 多模块后端(iot-common/device/rule/data/alarm/auth)
- frontend/admin/  Vue3 + Element Plus 管理后台
- Android/   涂鸦原生 APP (com.xhai.iot)
- backend/sql/  Flyway 风格迁移脚本(V2~V21)
- docs/       对接文档与设计文档

排除范围:
- 构建产物: node_modules/ target/ build/ dist/ .gradle/
- 签名密钥: *.jks *.keystore (严禁入库)
- 本机配置: Android/local.properties
- 运行日志: logs/ *.log
- AI 协作本地数据 .codebuddy/ 与第三方 demo tuya-demo/

.gitattributes 统一以 LF 存储, Windows 批处理保留 CRLF
2026-10-27 12:00:41 +08:00

824 lines
90 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>MumIot API · 中控屏接口</title>
<style>
:root{
--bg:#eef2f7; --contentbg:#ffffff; --sidebar:#0f172a; --sidebar2:#15233f;
--text:#1e293b; --muted:#64748b; --line:#e2e8f0; --line2:#cbd5e1;
--accent:#2563eb; --accent2:#1d4ed8;
--get:#16a34a; --post:#2563eb; --put:#d97706; --del:#dc2626;
--req:#dc2626; --opt:#0891b2; --none:#64748b;
--codebg:#0f172a; --codefg:#cbd5e1;
}
*{box-sizing:border-box}
html,body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);}
body{background:var(--bg);}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
.layout{display:flex;min-height:100vh}
/* ===== 左侧导航 ===== */
aside#sidebar{width:264px;flex:0 0 264px;background:var(--sidebar);color:#cbd5e1;position:sticky;top:0;height:100vh;overflow-y:auto;padding:0 0 30px}
#sidebar .brand{padding:20px 22px 16px;border-bottom:1px solid #1e293b;margin-bottom:8px}
#sidebar .brand h2{margin:0;font-size:16px;color:#fff;letter-spacing:.02em}
#sidebar .brand p{margin:5px 0 0;font-size:12px;color:#94a3b8}
#sidebar .grp{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#5b6b85;padding:16px 22px 5px}
#sidebar a.nav{display:block;padding:8px 22px;font-size:13.5px;color:#cbd5e1;text-decoration:none;border-left:3px solid transparent}
#sidebar a.nav:hover{background:var(--sidebar2);color:#fff}
#sidebar a.nav.active{background:var(--sidebar2);color:#fff;border-left-color:var(--accent)}
#sidebar .ft{padding:18px 22px 0;font-size:11px;color:#5b6b85;border-top:1px solid #1e293b;margin-top:18px}
/* ===== 主区域 ===== */
main{flex:1;min-width:0;display:flex;flex-direction:column}
#topbar{position:sticky;top:0;z-index:5;background:rgba(255,255,255,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:13px 30px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
#topbar .ptitle{font-size:18px;font-weight:700;margin-right:auto;white-space:nowrap}
#topbar input.search{flex:1;min-width:170px;max-width:300px;padding:9px 12px;border:1px solid var(--line);border-radius:9px;font-size:13px;outline:none;background:#fff}
#topbar input.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,.12)}
.basebox{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);white-space:nowrap}
.basebox input{padding:8px 10px;border:1px solid var(--line);border-radius:9px;font-size:12.5px;width:280px;max-width:46vw;background:#fff}
#content{padding:26px 30px 90px;max-width:1100px;width:100%}
/* ===== 概览页 ===== */
.ov-hero{background:linear-gradient(120deg,#0ea5e9,#6366f1);color:#fff;border-radius:14px;padding:26px 28px;margin-bottom:22px}
.ov-hero h1{margin:0 0 8px;font-size:24px}
.ov-hero p{margin:0;opacity:.92;font-size:14px;line-height:1.7}
.ov-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px;margin-bottom:26px}
.ov-card{background:var(--contentbg);border:1px solid var(--line);border-radius:12px;padding:16px 18px;transition:.15s}
.ov-card:hover{border-color:var(--accent);box-shadow:0 4px 16px rgba(37,99,235,.1)}
.ov-card h3{margin:0 0 6px;font-size:15px}
.ov-card p{margin:0 0 10px;font-size:12.5px;color:var(--muted);line-height:1.6}
.ov-card .cnt{font-size:12px;color:var(--accent);font-weight:600}
.ov-note{background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:16px 18px;font-size:13px;line-height:1.8;margin-bottom:22px}
.ov-note b{color:#c2410c}
.ov-note code{background:#fff;border:1px solid var(--line);border-radius:5px;padding:1px 6px;font-size:12px}
/* ===== 分组 / 章节 ===== */
.sec{margin:0 0 30px}
.sec>.h2{font-size:19px;border-left:4px solid var(--accent);padding-left:11px;margin:0 0 4px}
.sec>.sub{color:var(--muted);font-size:13px;margin:0 0 14px}
.toc{background:var(--contentbg);border:1px solid var(--line);border-radius:10px;padding:10px 14px;margin:0 0 20px;font-size:13px;display:flex;flex-wrap:wrap;gap:8px 14px}
.toc a{padding:2px 10px;border:1px solid var(--line);border-radius:20px;color:var(--muted)}
.toc a:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
/* ===== 接口卡片 ===== */
.ep{background:var(--contentbg);border:1px solid var(--line);border-radius:12px;padding:15px 17px;margin:0 0 13px;scroll-margin-top:80px}
.ep.hidden{display:none}
.ep .row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.badge{font-weight:700;font-size:11.5px;padding:3px 9px;border-radius:6px;color:#fff}
.m-GET{background:var(--get)} .m-POST{background:var(--post)} .m-PUT{background:var(--put)} .m-DELETE{background:var(--del)}
.path{font-family:"SFMono-Regular",Consolas,monospace;font-size:13.5px;color:#0f172a;word-break:break-all;font-weight:600}
.tag{font-size:11px;padding:2px 9px;border-radius:20px;border:1px solid var(--line)}
.t-req{color:var(--req);border-color:var(--req)} .t-opt{color:var(--opt);border-color:var(--opt)} .t-none{color:var(--none);border-color:var(--none)}
.ep .desc{margin:9px 0 6px;font-size:14px}
.ep .params{font-size:12.5px;color:var(--muted)}
.ep .params b{color:var(--text)}
.ep .mlink{font-size:12.5px;margin-top:8px}
.ep .mlink a{color:var(--accent)}
.ex{margin-top:8px}
.ex summary{cursor:pointer;font-size:12.5px;color:var(--accent);user-select:none;outline:none}
.ex pre{background:var(--codebg);border:1px solid #1e293b;border-radius:9px;padding:12px 14px;overflow:auto;
font-family:"SFMono-Regular",Consolas,monospace;font-size:12px;color:var(--codefg);margin:8px 0 0;white-space:pre;line-height:1.55}
.ex pre .c{color:#64748b}
.empty{color:var(--muted);text-align:center;padding:40px;display:none}
/* ===== 模型 ===== */
.model{background:var(--contentbg);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:0 0 16px;scroll-margin-top:80px}
.model h3{margin:0 0 4px;font-size:16px;color:#0f172a}
.model .mtag{font-size:11px;color:var(--muted);margin:0 0 10px}
.model .mtag .q{color:var(--accent);background:#eff6ff;border:1px solid #bfdbfe;border-radius:5px;padding:1px 6px;margin-right:6px}
.model .exjson{margin-top:8px}
table.ft{width:100%;border-collapse:collapse;font-size:12.5px;margin:6px 0 10px}
table.ft th{background:#f1f5f9;text-align:left;color:var(--muted);font-weight:600;padding:7px 9px;border-bottom:2px solid var(--line)}
table.ft td{padding:7px 9px;border-bottom:1px solid #eef2f7;vertical-align:top}
table.ft td .ty{color:#0369a1;font-family:Consolas,monospace;font-size:12px}
table.ft td .rq{color:var(--req);font-weight:700}
table.ft td .op{color:var(--muted)}
.model .exjson summary{cursor:pointer;font-size:12.5px;color:var(--accent);outline:none}
.model .exjson pre{background:var(--codebg);border-radius:9px;padding:12px 14px;overflow:auto;font-family:Consolas,monospace;font-size:12px;color:var(--codefg);margin:8px 0 0;white-space:pre}
.baseblk{background:#f8fafc;border:1px solid var(--line);border-radius:8px;padding:10px 12px;font-size:12px;margin:6px 0 10px}
.baseblk b{color:#0f172a}
.resp-note{font-size:12.5px;color:var(--muted);margin-top:6px}
code.inl{background:#f1f5f9;border:1px solid var(--line);border-radius:5px;padding:1px 6px;font-size:12px;font-family:Consolas,monospace;color:#0f172a}
footer{color:var(--muted);font-size:12px;text-align:center;padding:24px;border-top:1px solid var(--line);margin-top:20px}
</style>
</head>
<body>
<div class="layout">
<aside id="sidebar"></aside>
<main>
<div id="topbar"></div>
<div id="content"></div>
</main>
</div>
<script>
/* ===== MumIot API 文档 · 共享脚本 ===== */
/* 👇 只需在这里改一次默认 Base URL(生产用反向代理域名) */
const DEFAULT_BASE="http://mum.tonzou.cn/api/v1";
/* 👇 产品名(用于标题/品牌/首页),只改这一处 */
const PRODUCT_NAME="MumIot";
const AUTH={req:"强制",opt:"可选",none:"无"};
function F(n,t,r,d){return [n,t,r,d];}
const BASE_FIELDS=[
F("id","Long","-","主键(雪花ID, @TableId ASSIGN_ID)"),
F("createTime","LocalDateTime","-","创建时间(自动填充)"),
F("updateTime","LocalDateTime","-","更新时间(自动填充)"),
F("deleted","Integer","-","逻辑删除(0未删/1已删, @TableLogic)"),
F("tenantId","Long","-","租户ID(自动填充)"),
F("createBy","Long","-","创建人ID(自动填充)"),
F("updateBy","Long","-","更新人ID(自动填充)")
];
/* ===== 数据模型(字段 + JSON 示例) ===== */
const MODELS={
LoginRequest:{title:"LoginRequest 登录请求体",fields:[F("username","String","是","用户名"),F("password","String","是","密码")],example:{username:"admin",password:"admin123"}},
RefreshRequest:{title:"RefreshRequest 刷新Token",fields:[F("refreshToken","String","是","刷新令牌")],example:{refreshToken:"eyJhbGciOiJIUzI1Ni...refresh"}},
RegisterRequest:{title:"RegisterRequest 普通用户注册",fields:[F("username","String","是","用户名(3-32,字母数字下划线)"),F("password","String","是","密码(6-20)"),F("nickname","String","是","昵称"),F("phone","String","否","手机号(1[3-9]\\d{9})")],example:{username:"alice",password:"abc123",nickname:"Alice",phone:"13800138000"}},
MemberRegisterRequest:{title:"MemberRegisterRequest 会员注册",fields:[F("username","String","是","用户名(3-50)"),F("password","String","是","密码(6-32)"),F("nickname","String","否","昵称(≤50)"),F("phone","String","否","手机号(≤20)")],example:{username:"member01",password:"abc123",nickname:"小明",phone:"13800138000"}},
PasswordChange:{title:"PasswordChange 修改密码",fields:[F("oldPassword","String","是","原密码"),F("newPassword","String","是","新密码")],example:{oldPassword:"abc123",newPassword:"newPass456"}},
OauthCallback:{title:"OauthCallback 涂鸦OAuth回调",fields:[F("code","String","是","授权码code")],example:{code:"a1b2c3d4"}},
BindTokenRequest:{title:"BindTokenRequest Token绑定",fields:[F("accessToken","String","是","涂鸦access_token")],example:{accessToken:"tok_xxxx"}},
SmsSendRequest:{title:"SmsSendRequest 发送验证码",fields:[F("phone","String","是","手机号"),F("countryCode","String","否","国家码(默认86)")],example:{phone:"13800138000",countryCode:"86"}},
SmsBindRequest:{title:"SmsBindRequest 验证码绑定",fields:[F("phone","String","是","手机号"),F("code","String","是","验证码"),F("countryCode","String","否","国家码(默认86)")],example:{phone:"13800138000",code:"123456",countryCode:"86"}},
UserRequest:{title:"UserRequest 后台用户(增/改)",fields:[F("username","String","是","用户名(3-64)"),F("password","String","否","密码(6-64,创建时必填)"),F("realName","String","否","真实姓名"),F("phone","String","否","手机号"),F("email","String","否","邮箱"),F("status","Integer","否","状态(0禁用/1启用,默认1)"),F("roleIds","List<Long>","否","角色ID列表")],example:{username:"operator",password:"abc123",realName:"运维",phone:"13800138000",email:"op@example.com",status:1,roleIds:[2,3]}},
RoleRequest:{title:"RoleRequest 后台角色(增/改)",fields:[F("roleName","String","是","角色名称(≤64)"),F("roleCode","String","是","角色编码(≤64)"),F("description","String","否","描述"),F("status","Integer","否","状态(0禁用/1启用,默认1)"),F("sort","Integer","否","排序(默认0)"),F("permissions","Set<String>","否","权限编码集合")],example:{roleName:"设备管理员",roleCode:"DEVICE_ADMIN",description:"管理设备",status:1,sort:0,permissions:["device:view","device:edit"]}},
MemberRequest:{title:"MemberRequest 会员(增/改)",fields:[F("username","String","是","用户名(3-64)"),F("password","String","否","密码(6-64,创建时必填)"),F("nickname","String","否","昵称"),F("phone","String","否","手机号"),F("email","String","否","邮箱"),F("gender","Integer","否","性别(0未知/1男/2女)"),F("status","Integer","否","状态(0禁用/1正常,默认1)")],example:{username:"member01",password:"abc123",nickname:"小明",phone:"13800138000",email:"m@example.com",gender:1,status:1}},
PlatformConfigRequest:{title:"PlatformConfigRequest 第三方平台配置",fields:[F("platformCode","String","是","平台编码(tuya/google/alexa/ali...)"),F("platformName","String","否","平台名称"),F("enabled","Integer","否","是否启用(0/1)"),F("accessId","String","否","涂鸦Access ID"),F("accessSecret","String","否","涂鸦Access Secret"),F("apiKey","String","否","大模型API Key"),F("modelName","String","否","大模型(qwen-turbo/plus/max)"),F("baseUrl","String","否","API基础地址(OpenAI兼容)"),F("timeoutMs","Integer","否","超时(毫秒)"),F("apiRegion","String","否","API区域(cn/us/eu/in)"),F("projectId","String","否","Google Home Project ID"),F("clientId","String","否","Client ID"),F("clientSecret","String","否","Client Secret"),F("appIdentifier","String","否","App授权签名标识符"),F("alexaClientId","String","否","Alexa Client ID"),F("alexaSecret","String","否","Alexa Secret"),F("region","String","否","服务区域"),F("remark","String","否","备注")],example:{platformCode:"tuya",platformName:"涂鸦开放平台",enabled:1,accessId:"your_access_id",accessSecret:"your_access_secret",apiKey:"sk-xxxx",modelName:"qwen-turbo",baseUrl:"https://dashscope.aliyuncs.com",timeoutMs:30000}},
Device:{title:"Device 设备实体",entity:true,fields:[F("deviceId","String","否","设备唯一标识"),F("deviceName","String","否","设备名称"),F("platform","String","否","平台(tuya/miot/dueros/local/matter)"),F("deviceType","String","否","设备类型"),F("platformProductId","String","否","平台原始产品ID(如涂鸦productId)"),F("firmwareVersion","String","否","固件版本"),F("status","String","否","状态(online/offline/error)"),F("capabilities","JsonNode","否","设备能力描述(JSON)"),F("location","String","否","设备位置"),F("groupId","Long","否","分组ID"),F("parentDeviceId","String","否","父网关设备ID"),F("isGateway","Integer","否","是否网关(1=网关)"),F("ownerId","Long","否","归属用户ID(member.id)"),F("productId","Long","否","所属产品ID"),F("homeId","Long","否","所属家庭ID"),F("roomId","Long","否","所属房间ID"),F("icon","String","否","图标URL"),F("remark","String","否","备注")],example:{deviceId:"DEV_001",deviceName:"客厅灯",platform:"tuya",deviceType:"light",platformProductId:"abc123",firmwareVersion:"1.0.0",status:"online",capabilities:{"switch":true,"brightness":80},location:"客厅",groupId:1,parentDeviceId:null,isGateway:0,ownerId:1001,productId:10,homeId:1,roomId:2,icon:"https://cdn/x/light.png",remark:""}},
DeviceCommand:{title:"DeviceCommand 控制指令",fields:[F("deviceId","String","否","设备ID"),F("commandType","String","否","指令类型(switch/set/query...)"),F("params","Map<String,Object>","否","指令参数(如{\"power\":\"on\"}/{\"bright_value\":80})"),F("timeout","Long","否","超时毫秒(默认30000)")],example:{deviceId:"DEV_001",commandType:"switch",params:{power:"on"},timeout:30000}},
DeviceConnectRequest:{title:"DeviceConnectRequest 设备连接",fields:[F("deviceId","String","否","设备ID"),F("platform","String","否","平台(tuya/miot/...)"),F("credentials","Credentials","否","认证凭证(见下)")],nested:[["credentials.accessToken","String","否","访问令牌(如Tuya accessToken)"],["credentials.username","String","否","用户名"],["credentials.password","String","否","密码"],["credentials.clientId","String","否","客户端ID"],["credentials.extra","Map<String,Object>","否","其它扩展属性"]],example:{deviceId:"DEV_001",platform:"tuya",credentials:{accessToken:"tok_xxxx",username:"",password:"",clientId:"",extra:{}}}},
DeviceActivityReport:{title:"DeviceActivityReport 控制动态上报",fields:[F("action","String","否","动作(switch_on/switch_off/adjust/status_change)"),F("actionDesc","String","否","人类可读描述(可空,后端生成)"),F("detail","Map<String,Object>","否","明细(下发的DP,如{\"1\":true})"),F("commandId","String","否","控制回执配对ID(可空)")],example:{action:"switch_on",actionDesc:"开启客厅灯",detail:{"1":true},commandId:"cmd_1700000000"}},
DeviceGroup:{title:"DeviceGroup 设备分组实体",entity:true,fields:[F("groupName","String","否","分组名称"),F("groupCode","String","否","分组编码"),F("parentId","Long","否","父分组ID(0=根节点)"),F("description","String","否","描述"),F("sort","Integer","否","排序"),F("status","Integer","否","状态(0停用/1正常)")],example:{groupName:"卧室",groupCode:"bedroom",parentId:0,description:"",sort:0,status:1}},
Product:{title:"Product 产品实体",entity:true,fields:[F("productKey","String","否","产品业务编码"),F("name","String","否","产品名称"),F("category","String","否","品类(light/switch/ac/security...)"),F("manufacturer","String","否","厂商"),F("model","String","否","型号"),F("platform","String","否","接入平台(tuya/local/matter)"),F("platformProductId","String","否","平台产品ID(涂鸦productId)"),F("connectType","String","否","联网方式(wifi/ble/zigbee/matter)"),F("netConfigType","String","否","配网方式"),F("icon","String","否","图标"),F("description","String","否","说明"),F("status","String","否","状态(draft/published)"),F("specVersion","Integer","否","物模型版本")],example:{productKey:"PK_LIGHT_001",name:"智能灯",category:"light",manufacturer:"Tuya",model:"L1",platform:"tuya",platformProductId:"abc123",connectType:"wifi",netConfigType:"ap",icon:"https://cdn/x/light.png",description:"支持开关/亮度/色温",status:"published",specVersion:1}},
ProductSpec:{title:"ProductSpec 物模型(TSL)",fields:[F("productId","Long","否","产品ID"),F("properties","List<ProductProperty>","否","属性列表"),F("events","List<ProductEvent>","否","事件列表"),F("actions","List<ProductAction>","否","服务列表")],example:{productId:10,properties:[{code:"switch_1",name:"开关",access:"write",type:"bool",dpId:"1"}],events:[],actions:[]}},
ProductProperty:{title:"ProductProperty 物模型-属性",entity:true,fields:[F("productId","Long","否","产品ID"),F("code","String","否","功能点编码(如switch_1)"),F("name","String","否","名称"),F("access","String","否","访问权限(read/write/notify)"),F("type","String","否","数据类型(bool/int/enum/float/string/json)"),F("min","String","否","最小值"),F("max","String","否","最大值"),F("step","String","否","步长"),F("unit","String","否","单位"),F("enumList","String","否","枚举值(JSON数组)"),F("dpId","String","否","平台DP点映射(涂鸦DP)"),F("defaultValue","String","否","默认值"),F("sort","Integer","否","排序")],example:{productId:10,code:"switch_1",name:"开关",access:"write",type:"bool",min:"0",max:"1",step:"1",unit:"",enumList:"",dpId:"1",defaultValue:"false",sort:1}},
ProductEvent:{title:"ProductEvent 物模型-事件",entity:true,fields:[F("productId","Long","否","产品ID"),F("code","String","否","事件编码"),F("name","String","否","名称"),F("type","String","否","类型(info/alert/fault)"),F("params","String","否","事件参数(JSON)")],example:{productId:10,code:"power_report",name:"电量上报",type:"info",params:"{}"}},
ProductAction:{title:"ProductAction 物模型-服务",entity:true,fields:[F("productId","Long","否","产品ID"),F("code","String","否","服务编码"),F("name","String","否","名称"),F("input","String","否","输入参数(JSON)"),F("output","String","否","输出参数(JSON)")],example:{productId:10,code:"restart",name:"重启",input:"{}",output:"{}"}},
Firmware:{title:"Firmware 固件实体",entity:true,fields:[F("productId","Long","否","产品ID"),F("version","String","否","固件版本"),F("fileUrl","String","否","升级包地址"),F("fileMd5","String","否","校验MD5"),F("size","Long","否","大小(字节)"),F("releaseNote","String","否","更新说明"),F("isLatest","Integer","否","是否最新(0/1)"),F("status","String","否","状态(draft/published)")],example:{productId:10,version:"1.2.0",fileUrl:"https://cdn/x/fw_1.2.0.bin",fileMd5:"a1b2c3d4",size:123456,releaseNote:"修复已知问题",isLatest:0,status:"draft"}},
OtaTaskCreateRequest:{title:"OtaTaskCreateRequest 创建OTA任务",fields:[F("firmwareId","Long","是","目标固件ID"),F("scope","String","否","范围(all/designated/gray)"),F("deviceIds","List<Long>","否","指定设备ID(scope=designated必填)"),F("grayPercent","Integer","否","灰度百分比(scope=gray时1-99)")],example:{firmwareId:5,scope:"designated",deviceIds:[101,102],grayPercent:10}},
OtaReportRequest:{title:"OtaReportRequest OTA结果上报",fields:[F("deviceId","String","是","设备ID"),F("success","Boolean","是","是否升级成功"),F("version","String","否","升级后版本(成功时上报)")],example:{deviceId:"DEV_001",success:true,version:"1.2.0"}},
HomeRequest:{title:"HomeRequest 家庭(增/改)",fields:[F("name","String","是","家庭名称(≤30)"),F("coverImage","String","否","封面图URL"),F("address","String","否","地址(≤100)")],example:{name:"我的家",coverImage:"https://cdn/x/home.png",address:"上海市浦东新区"}},
JoinHomeRequest:{title:"JoinHomeRequest 加入家庭",fields:[F("shareCode","String","是","分享码(6-12)")],example:{shareCode:"ABC123"}},
MemberRoleRequest:{title:"MemberRoleRequest 修改成员角色",fields:[F("role","String","是","目标角色(admin/member)")],example:{role:"admin"}},
RoomRequest:{title:"RoomRequest 房间(增/改)",fields:[F("name","String","是","房间名称")],example:{name:"主卧"}},
TuyaBindRequest:{title:"TuyaBindRequest 绑定涂鸦设备",fields:[F("homeId","Long","是","我们的家庭ID"),F("roomId","Long","否","房间ID"),F("deviceIds","List<String>","否","涂鸦设备ID列表(旧版兼容)"),F("devices","List<DeviceInfo>","否","设备详情(推荐)"),F("fullSync","Boolean","否","全量同步(默认false)")],nested:[["devices.deviceId","String","否","涂鸦设备ID"],["devices.deviceName","String","否","设备名称"],["devices.category","String","否","设备分类"],["devices.online","Boolean","否","是否在线"],["devices.productId","String","否","产品ID"],["devices.icon","String","否","图标URL"]],example:{homeId:1,roomId:2,deviceIds:["tdev1"],devices:[{deviceId:"tdev1",deviceName:"灯",category:"light",online:true,productId:"pid",icon:""}],fullSync:false}},
TuyaNativeBindRequest:{title:"TuyaNativeBindRequest 原生SDK绑定",fields:[F("tuyaUid","String","是","涂鸦用户UID"),F("account","String","是","登录账号(手机号/邮箱)"),F("loginMethod","String","否","登录方式(phone/email,默认phone)"),F("platform","String","否","平台(android/ios/h5,默认android)")],example:{tuyaUid:"uid123",account:"13800138000",loginMethod:"phone",platform:"android"}},
TuyaRename:{title:"TuyaRename 重命名涂鸦设备",fields:[F("deviceName","String","是","新设备名称")],example:{deviceName:"客厅主灯"}},
TuyaCommand:{title:"TuyaCommand 涂鸦指令数组",fields:[F("code","String","是","指令码(如switch_1/bright_value)"),F("value","Object","是","指令值(true/80/\"white\"等)")],example:[{code:"switch_1",value:true},{code:"bright_value",value:80}]},
DeviceStatusSyncRequest:{title:"DeviceStatusSyncRequest 状态同步",fields:[F("deviceId","String","否","涂鸦设备ID"),F("online","Boolean","否","是否在线"),F("dps","Map<String,Object>","否","DP状态(key=dpId,value=dp值)"),F("source","String","否","上报来源(panel/app)")],example:{deviceId:"tdev1",online:true,dps:{"1":true,"2":80},source:"app"}},
DeviceDataPoint:{title:"DeviceDataPoint 数据点(数组元素)",fields:[F("key","String","是","数据点key"),F("type","String","否","类型(number/string/boolean/...)"),F("value","Object","是","数据值"),F("timestamp","Long","否","时间戳(毫秒)"),F("quality","String","否","数据质量(good/bad)")],example:{key:"temperature",type:"number",value:26.5,timestamp:1697000000000,quality:"good"}},
RuleEntity:{title:"RuleEntity 规则实体",entity:true,fields:[F("sceneId","Long","否","所属场景ID(空=独立规则)"),F("ruleName","String","否","规则名称"),F("ruleCode","String","否","规则编码"),F("description","String","否","规则描述"),F("triggerDeviceId","String","否","触发设备ID"),F("conditionExpression","String","否","条件表达式"),F("actions","JsonNode","否","执行动作(JSON)"),F("enabled","Boolean","否","是否启用"),F("priority","Integer","否","优先级"),F("ownerId","Long","否","归属用户ID(member.id)")],example:{sceneId:null,ruleName:"温度告警",ruleCode:"R001",description:"",triggerDeviceId:"DEV_001",conditionExpression:"temp>30",actions:{type:"notify",content:"高温"},enabled:true,priority:1,ownerId:1001}},
SceneEntity:{title:"SceneEntity 场景实体",entity:true,fields:[F("sceneName","String","否","场景名称"),F("sceneCode","String","否","场景编码"),F("sceneType","String","否","类型(one_click/automation)"),F("cronExpression","String","否","定时cron(六段式,可空)"),F("conditions","String","否","触发条件(JSON数组)"),F("actions","String","否","执行动作(JSON数组)"),F("conditionLogic","String","否","多条件逻辑(AND/OR)"),F("icon","String","否","图标URL"),F("description","String","否","描述"),F("enabled","Boolean","否","是否启用"),F("sortOrder","Integer","否","排序"),F("ownerId","Long","否","归属用户ID(member.id)")],example:{sceneName:"回家模式",sceneCode:"S001",sceneType:"one_click",cronExpression:"",conditions:"[]",actions:"[{\"type\":\"set_property\"}]",conditionLogic:"AND",icon:"",description:"",enabled:true,sortOrder:0,ownerId:1001}},
SceneCreateRequest:{title:"SceneCreateRequest 结构化场景(增/改)",fields:[F("sceneName","String","否","场景名称"),F("ownerId","Long","否","归属用户ID(网关注入)"),F("sceneType","String","否","类型(one_click/automation,可空)"),F("icon","String","否","图标"),F("description","String","否","描述"),F("conditionLogic","String","否","多条件逻辑(AND/OR,默认AND)"),F("conditions","List<SceneConditionDTO>","否","触发条件列表"),F("actions","List<SceneActionDTO>","否","执行动作列表")],example:{sceneName:"回家模式",ownerId:1001,sceneType:"one_click",icon:"",description:"",conditionLogic:"AND",conditions:[{type:"device_state",deviceId:"DEV_001",propertyCode:"switch_1",operator:">",value:true}],actions:[{type:"set_property",deviceId:"DEV_002",propertyCode:"switch_1",value:true},{type:"delay",seconds:3},{type:"notify",channel:"push",content:"已开启"}]}},
SceneActionDTO:{title:"SceneActionDTO 场景动作",fields:[F("type","String","是","动作类型(set_property/delay/execute_scene/notify)"),F("deviceId","String","否","目标设备ID(set_property用)"),F("propertyCode","String","否","属性code(set_property用)"),F("value","Object","否","属性值(set_property用)"),F("seconds","Integer","否","延时秒数(delay用)"),F("sceneId","Long","否","目标场景ID(execute_scene用)"),F("channel","String","否","通知渠道(notify用:app/sms/push)"),F("content","String","否","通知内容(notify用)")],example:{type:"set_property",deviceId:"DEV_002",propertyCode:"switch_1",value:true,seconds:3,sceneId:1,channel:"push",content:"已开启"}},
SceneConditionDTO:{title:"SceneConditionDTO 场景条件",fields:[F("type","String","是","条件类型(device_state/timer/manual)"),F("deviceId","String","否","设备ID(device_state必填)"),F("propertyCode","String","否","属性code(平台无关)"),F("dpId","String","否","平台DP标识(兼容dp-id)"),F("operator","String","否","运算符(==,!=,>,<,>=,<=)"),F("value","Object","否","比较值(boolean/number/string)"),F("time","String","否","定时时间HH:mm(timer用)"),F("weekDays","List<Integer>","否","定时重复星期1-7(timer用)")],example:{type:"device_state",deviceId:"DEV_001",propertyCode:"switch_1",dpId:"1",operator:">",value:true,time:"08:00",weekDays:[1,2,3]}},
AlarmRuleEntity:{title:"AlarmRuleEntity 告警规则实体",entity:true,fields:[F("ruleName","String","否","规则名称"),F("ruleCode","String","否","规则编码"),F("deviceId","String","否","设备ID(空=所有设备)"),F("dataPoint","String","否","数据点"),F("conditionExpression","String","否","条件表达式"),F("alarmLevel","String","否","告警级别"),F("alarmTemplate","String","否","告警消息模板"),F("enabled","Boolean","否","是否启用"),F("deduplicationWindow","Integer","否","去重窗口(分钟)"),F("notificationMethods","String","否","通知方式(email/sms/websocket/push)"),F("ownerId","Long","否","归属用户ID(member.id)")],example:{ruleName:"高温告警",ruleCode:"A001",deviceId:"DEV_001",dataPoint:"temperature",conditionExpression:"value>30",alarmLevel:"high",alarmTemplate:"设备{deviceName}温度过高",enabled:true,deduplicationWindow:30,notificationMethods:"push,email",ownerId:1001}},
AiCommandRequest:{title:"AiCommandRequest 自然语言指令",fields:[F("text","String","是","指令文本(如:打开客厅的灯)"),F("targetType","String","否","二次确认目标类型(device/scene)"),F("targetId","String","否","二次确认目标ID"),F("learn","Boolean","否","是否学习为别名(默认true)")],example:{text:"打开客厅的灯",targetType:"device",targetId:"DEV_001",learn:true}},
AiAliasRequest:{title:"AiAliasRequest 别名维护",fields:[F("alias","String","是","用户说法(如:客厅开关)"),F("targetType","String","是","目标类型(device/scene/room)"),F("targetId","String","是","目标ID(device.device_id/scene.id/room.id)"),F("homeId","Long","否","归属家庭ID(可选)")],example:{alias:"客厅灯",targetType:"device",targetId:"DEV_001",homeId:1}},
AiDeviceProfileRequest:{title:"AiDeviceProfileRequest 指令码校准",fields:[F("powerCode","String","否","开/关指令码(如switch_1/switch_led/control)"),F("powerOnValue","String","否","开启值(true/1)"),F("powerOffValue","String","否","关闭值(false/0)"),F("brightnessCode","String","否","亮度指令码(如bright_value)"),F("colorTempCode","String","否","色温指令码(如temp_value)"),F("temperatureCode","String","否","温度设定码(如temp_set)"),F("modeCode","String","否","模式指令码(如mode)")],example:{powerCode:"switch_1",powerOnValue:"true",powerOffValue:"false",brightnessCode:"bright_value",colorTempCode:"temp_value",temperatureCode:"temp_set",modeCode:"mode"}},
UserQuery:{title:"UserQuery 用户查询参数",query:true,fields:[F("username","String","否","用户名"),F("realName","String","否","真实姓名"),F("roleId","Long","否","角色ID"),F("status","Integer","否","状态(0/1)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20&status=1"},
RoleQuery:{title:"RoleQuery 角色查询参数",query:true,fields:[F("roleName","String","否","角色名称(模糊)"),F("roleCode","String","否","角色编码"),F("status","Integer","否","状态(0/1)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20"},
MemberQuery:{title:"MemberQuery 会员查询参数",query:true,fields:[F("keyword","String","否","用户名/昵称(模糊)"),F("phone","String","否","手机号"),F("status","Integer","否","状态(0/1)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20&keyword=小"},
LogQuery:{title:"LogQuery 日志查询参数",query:true,fields:[F("username","String","否","操作人(模糊)"),F("module","String","否","模块"),F("operation","String","否","操作类型"),F("status","Integer","否","状态(0失败/1成功)"),F("startTime","LocalDateTime","否","开始时间"),F("endTime","LocalDateTime","否","结束时间"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20&module=device"},
DeviceQuery:{title:"DeviceQuery 设备查询参数",query:true,fields:[F("deviceName","String","否","设备名称(模糊)"),F("platform","String","否","平台(tuya/miot/...)"),F("deviceType","String","否","设备类型"),F("status","String","否","状态(online/offline/error)"),F("ownerId","Long","否","归属用户ID"),F("homeId","Long","否","家庭ID"),F("roomId","Long","否","房间ID"),F("groupId","Long","否","分组ID"),F("deviceId","String","否","设备ID(模糊)"),F("parentDeviceId","String","否","父网关ID"),F("isGateway","Integer","否","是否网关(1)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20&platform=tuya"},
ProductQuery:{title:"ProductQuery 产品查询参数",query:true,fields:[F("page","Integer","否","页码(默认1)"),F("size","Integer","否","每页条数(默认20)"),F("keyword","String","否","名称/编码(模糊)"),F("category","String","否","品类"),F("platform","String","否","平台")],sample:"page=1&size=20&category=light"},
SceneQuery:{title:"SceneQuery 场景查询参数",query:true,fields:[F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20"},
GroupQuery:{title:"GroupQuery 分组查询参数",query:true,fields:[F("groupName","String","否","分组名称(模糊)"),F("status","Integer","否","状态(0/1)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20"},
AlarmQuery:{title:"AlarmQuery 告警查询参数",query:true,fields:[F("deviceId","String","否","设备ID"),F("alarmLevel","String","否","告警级别"),F("status","String","否","状态(pending/confirmed/resolved)"),F("ownerId","Long","否","归属用户ID"),F("startTime","LocalDateTime","否","开始时间"),F("endTime","LocalDateTime","否","结束时间"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20&status=pending"},
RuleQuery:{title:"RuleQuery 规则查询参数",query:true,fields:[F("ruleName","String","否","规则名称(模糊)"),F("enabled","Boolean","否","是否启用"),F("triggerDeviceId","String","否","触发设备ID"),F("ownerId","Long","否","归属用户ID"),F("sceneId","Long","否","所属场景ID"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pageNum=1&pageSize=20"},
DataHistoryQuery:{title:"DataHistoryQuery 历史数据查询参数",query:true,fields:[F("pointKey","String","否","数据点key(不填查全部)"),F("startTime","Long","否","开始时间戳(毫秒)"),F("endTime","Long","否","结束时间戳(毫秒)"),F("pageNum","Integer","否","页码(默认1)"),F("pageSize","Integer","否","每页条数(默认20)")],sample:"pointKey=temperature&startTime=1696900000000&endTime=1697000000000"},
DataStatsQuery:{title:"DataStatsQuery 聚合统计参数",query:true,fields:[F("field","String","是","统计字段(数据点key)"),F("aggregation","String","是","聚合方式(mean/max/min/sum/count)"),F("startTime","Long","否","开始时间戳(毫秒)"),F("endTime","Long","否","结束时间(毫秒)")],sample:"field=temperature&aggregation=max&startTime=1696900000000&endTime=1697000000000"},
LoginResponse:{title:"LoginResponse 登录响应 data",resp:true,fields:[F("token","String","-","JWT令牌"),F("username","String","-","用户名"),F("roles","List<String>","-","角色编码列表"),F("expiresIn","Long","-","有效期(秒)")],example:{token:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",username:"admin",roles:["ADMIN"],expiresIn:86400}},
PageResult:{title:"PageResult 分页响应 data",resp:true,fields:[F("records","List","-","当前页数据列表(元素为对应entity)"),F("total","Long","-","总条数"),F("pages","Long","-","总页数"),F("current","Long","-","当前页"),F("size","Long","-","每页条数")],example:{records:[{id:1,deviceId:"DEV_001",deviceName:"客厅灯",platform:"tuya",status:"online"}],total:9,pages:1,current:1,size:20}},
/* ===== 中控屏(Panel) 数据模型 ===== */
PanelSession:{title:"PanelSession 屏会话(响应)",resp:true,fields:[F("panelId","Long","-","屏ID"),F("homeId","Long","否","归属家庭(后台预置即绑定场景)"),F("role","String","-","角色(GATEWAY/DISPLAY)"),F("accessToken","String","-","屏访问令牌"),F("refreshToken","String","-","刷新令牌(轮换用)"),F("expiresIn","Long","-","有效期(秒)")],example:{panelId:1,homeId:1,role:"GATEWAY",accessToken:"eyJ...",refreshToken:"r-...",expiresIn:86400}},
PanelSessionReq:{title:"PanelSessionReq 建会话请求",fields:[F("bootstrapSecret","String","是","管理端预置的一次性初始化凭证")],example:{bootstrapSecret:"bs-xxxx"}},
PanelSessionRefreshReq:{title:"PanelSessionRefreshReq 刷新请求",fields:[F("refreshToken","String","是","刷新令牌")],example:{refreshToken:"r-xxxx"}},
PanelSessionRefreshResp:{title:"PanelSessionRefreshResp 刷新响应",resp:true,fields:[F("accessToken","String","-","新访问令牌"),F("refreshToken","String","-","新刷新令牌"),F("expiresIn","Long","-","有效期(秒)")],example:{accessToken:"eyJ...",refreshToken:"r-...",expiresIn:86400}},
PanelHeartbeat:{title:"PanelHeartbeat 心跳体",fields:[F("online","Boolean","否","是否在线(默认true)"),F("uptime","Long","否","运行时长(秒)"),F("freeMem","Long","否","空闲内存(字节)"),F("msg","String","否","附加信息")],example:{online:true,uptime:3600,freeMem:524288000}},
PanelHeartbeatResp:{title:"PanelHeartbeatResp 心跳响应",resp:true,fields:[F("ok","Boolean","-","续约成功")],example:{ok:true}},
PanelInfo:{title:"PanelInfo 屏信息",resp:true,fields:[F("panelId","Long","-","屏ID"),F("homeId","Long","否","归属家庭"),F("role","String","-","角色"),F("enabled","Boolean","-","是否启用"),F("online","Boolean","-","是否在线"),F("lastSeenAt","Long","否","最后心跳时间"),F("features","List<String>","否","能力列表")],example:{panelId:1,homeId:1,role:"GATEWAY",enabled:true,online:true,lastSeenAt:1697000000000,features:["device-control","scene"]}},
PanelSnapshotQuery:{title:"PanelSnapshotQuery 快照分页",query:true,fields:[F("pageToken","String","否","游标(首屏为空)"),F("limit","Integer","否","每页条数(默认200)")],sample:"pageToken=&limit=200"},
PanelSnapshot:{title:"PanelSnapshot 家庭快照",resp:true,fields:[F("home","Object","-","家庭对象{homeId,name}"),F("rooms","List","-","房间列表"),F("devices","List","-","设备列表(首屏全量)"),F("stateEpoch","Long","-","一致性游标"),F("pageToken","String","否","下一页游标"),F("hasMore","Boolean","-","是否还有更多")],example:{home:{homeId:1,name:"我的家"},rooms:[{roomId:1,name:"客厅"}],devices:[{deviceId:"DEV_001",deviceName:"客厅灯"}],stateEpoch:1700000000000,pageToken:"",hasMore:false}},
PanelConfig:{title:"PanelConfig 屏配置",resp:true,fields:[F("configJson","String","-","运营配置(JSON)"),F("version","Integer","-","配置版本"),F("checksum","String","-","校验和(屏端比对是否变更)")],example:{configJson:"{...}",version:3,checksum:"a1b2c3"}},
PanelDeviceRegister:{title:"PanelDeviceRegister 设备登记",fields:[F("deviceId","String","是","设备唯一标识"),F("deviceName","String","否","设备名称"),F("platform","String","否","平台(tuya/miot/local/matter)"),F("productId","Long","否","产品ID"),F("category","String","否","品类"),F("schema","Object","否","标准控制模型")],example:{deviceId:"DEV_001",deviceName:"客厅灯",platform:"tuya",productId:10,category:"light"}},
PanelDeviceRegisterResult:{title:"PanelDeviceRegisterResult 登记响应",resp:true,fields:[F("deviceId","String","-","设备ID"),F("registered","Boolean","-","是否新登记")],example:{deviceId:"DEV_001",registered:true}},
PanelSchemaUpdate:{title:"PanelSchemaUpdate 控制模型更新",fields:[F("schema","Object","是","标准控制模型(属性/事件/动作)")],example:{schema:{switch_1:{type:"bool"}}}},
PanelSchemaUpdateResult:{title:"PanelSchemaUpdateResult",resp:true,fields:[F("accepted","Boolean","-","接受")],example:{accepted:true}},
PanelReportBatch:{title:"PanelReportBatch 状态上报",fields:[F("deviceId","String","是","设备ID"),F("items","List","是","状态项[{key,value,ts}]")],example:{deviceId:"DEV_001",items:[{key:"switch_1",value:true,ts:1697000000000}]}},
PanelReportResult:{title:"PanelReportResult 上报响应",resp:true,fields:[F("accepted","Integer","-","已接受条数"),F("rejected","Integer","-","被拒条数")],example:{accepted:1,rejected:0}},
PanelCmdPullReq:{title:"PanelCmdPullReq 命令拉取",fields:[F("cursor","Long","否","已处理到的最大命令ID"),F("limit","Integer","否","拉取条数(默认20)")],example:{cursor:100,limit:20}},
PanelCmdPullResp:{title:"PanelCmdPullResp 命令拉取响应",resp:true,fields:[F("commands","List","-","待执行命令[{commandId,type,params}]")],example:{commands:[{commandId:101,type:"set_property",params:{switch_1:true}}]}},
PanelCmdStartReq:{title:"PanelCmdStartReq 申请执行",fields:[F("executorRevision","Long","否","执行端版本号")],example:{executorRevision:5}},
PanelCmdStartResp:{title:"PanelCmdStartResp",resp:true,fields:[F("accepted","Boolean","-","接受")],example:{accepted:true}},
PanelCmdReportReq:{title:"PanelCmdReportReq 命令回报",fields:[F("status","String","是","执行状态(SUCCESS/FAIL)"),F("result","Object","否","结果明细")],example:{status:"SUCCESS",result:{switch_1:true}}},
PanelCmdResult:{title:"PanelCmdResult 命令回报响应",resp:true,fields:[F("recorded","Boolean","-","已记录")],example:{recorded:true}},
PanelDiagnostic:{title:"PanelDiagnostic 诊断上报",fields:[F("type","String","是","类型(network/mqtt/ota/command/config)"),F("detail","Object","否","明细"),F("ts","Long","否","时间戳")],example:{type:"network",detail:{rssi:-50},ts:1697000000000}},
PanelEventQuery:{title:"PanelEventQuery 事件查询",query:true,fields:[F("cursor","Long","否","上次事件ID(默认0)"),F("timeout","Integer","否","最长阻塞毫秒(默认20000)"),F("limit","Integer","否","返回条数(默认50)")],sample:"cursor=0&timeout=20000&limit=50"},
PanelEventsResp:{title:"PanelEventsResp 增量事件",resp:true,fields:[F("events","List","-","事件列表[{id,type,payload}]"),F("nextCursor","Long","-","下一游标")],example:{events:[{id:11,type:"device_state",payload:{deviceId:"DEV_001"}}],nextCursor:11}},
PanelOtaNext:{title:"PanelOtaNext 待升级任务",resp:true,fields:[F("taskId","Long","否","任务ID(无则为null)"),F("version","String","否","目标版本"),F("fileUrl","String","否","升级包地址"),F("size","Long","否","大小(字节)"),F("md5","String","否","校验MD5")],example:{taskId:5,version:"1.2.0",fileUrl:"https://cdn/x.bin",size:123456,md5:"a1b2"}},
PanelSceneRunReport:{title:"PanelSceneRunReport 场景执行结果",fields:[F("sceneId","Long","是","场景ID"),F("runId","String","否","执行批次ID"),F("status","String","是","状态(SUCCESS/FAIL)"),F("detail","Object","否","明细")],example:{sceneId:1,runId:"r-1",status:"SUCCESS"}},
PanelSceneRunView:{title:"PanelSceneRunView",resp:true,fields:[F("runId","String","-","执行批次ID"),F("status","String","-","状态")],example:{runId:"r-1",status:"SUCCESS"}},
PanelBindReq:{title:"PanelBindReq 会员绑定请求",fields:[F("serialNumber","String","是","中控屏序列号(SN)"),F("homeId","Long","是","绑定到的家庭ID"),F("pairingCode","String","否","配对码(可选)")],example:{serialNumber:"SN-2024-0001",homeId:1}},
PanelBindResult:{title:"PanelBindResult 绑定结果",resp:true,fields:[F("panelId","Long","-","屏ID"),F("serialNumber","String","-","序列号"),F("homeId","Long","-","家庭ID"),F("memberId","Long","-","会员ID"),F("role","String","-","角色"),F("boundAt","Long","-","绑定时间")],example:{panelId:1,serialNumber:"SN-2024-0001",homeId:1,memberId:1001,role:"GATEWAY",boundAt:1697000000000}},
PanelBindable:{title:"PanelBindable 可绑定性预检",resp:true,fields:[F("exists","Boolean","-","是否已预置"),F("enabled","Boolean","-","是否启用"),F("bound","Boolean","-","是否已绑定家庭"),F("panelId","Long","否","屏ID(已预置时)")],example:{exists:true,enabled:true,bound:false,panelId:1}},
PanelSummary:{title:"PanelSummary 我的屏",resp:true,fields:[F("panelId","Long","-","屏ID"),F("serialNumber","String","-","序列号"),F("homeId","Long","-","家庭ID"),F("role","String","-","角色"),F("enabled","Boolean","-","是否启用"),F("online","Boolean","-","是否在线"),F("lastSeenAt","Long","否","最后心跳")],example:{panelId:1,serialNumber:"SN-2024-0001",homeId:1,role:"GATEWAY",enabled:true,online:true,lastSeenAt:1697000000000}},
PanelAdminCreate:{title:"PanelAdminCreate 预置屏终端",fields:[F("serialNumber","String","是","序列号(SN)"),F("homeId","Long","否","家庭ID(留空=待绑定库存)"),F("memberId","Long","否","预绑会员(可选)"),F("role","String","否","角色(GATEWAY/DISPLAY,默认GATEWAY)"),F("model","String","否","型号"),F("features","List<String>","否","能力列表")],example:{serialNumber:"SN-2024-0001",homeId:null,role:"GATEWAY"}},
PanelAdminCreateResult:{title:"PanelAdminCreateResult 预置响应",resp:true,fields:[F("panelId","Long","-","屏ID"),F("serialNumber","String","-","序列号"),F("bootstrapSecret","String","-","一次性初始化凭证(仅返回一次)")],example:{panelId:1,serialNumber:"SN-2024-0001",bootstrapSecret:"bs-xxxx"}},
PanelAdminItem:{title:"PanelAdminItem 屏终端列表项",resp:true,fields:[F("panelId","Long","-","屏ID"),F("serialNumber","String","-","序列号"),F("homeId","Long","否","家庭ID"),F("role","String","-","角色"),F("enabled","Boolean","-","是否启用"),F("bound","Boolean","-","是否已绑定"),F("createdAt","Long","-","创建时间")],example:{panelId:1,serialNumber:"SN-2024-0001",homeId:null,role:"GATEWAY",enabled:true,bound:false,createdAt:1697000000000}},
PanelAdminRotateResult:{title:"PanelAdminRotateResult 轮换响应",resp:true,fields:[F("panelId","Long","-","屏ID"),F("bootstrapSecret","String","-","新初始化凭证")],example:{panelId:1,bootstrapSecret:"bs-new"}},
PanelConfigSave:{title:"PanelConfigSave 保存屏配置",fields:[F("configJson","String","是","配置JSON"),F("version","Integer","是","版本(需等于当前版本)")],example:{configJson:"{...}",version:3}},
PanelReassignReq:{title:"PanelReassignReq 多屏归属交接",fields:[F("fromPanelId","Long","是","源屏ID"),F("toPanelId","Long","是","目标屏ID"),F("routeRevision","Long","是","路由版本(需一致)")],example:{fromPanelId:1,toPanelId:2,routeRevision:7}},
PanelReassignResult:{title:"PanelReassignResult 交接响应",resp:true,fields:[F("routeRevision","Long","-","新路由版本"),F("reassigned","Integer","-","交接设备数")],example:{routeRevision:8,reassigned:3}}
};
/* ===== curl 示例模板 ===== */
const EXAMPLES={
login:(b)=>`<span class="c"># 1) 登录拿 Token</span>
curl -X POST ${b}/auth/login \\
-H 'Content-Type: application/json' \\
-d '{"username":"admin","password":"admin123"}'
<span class="c"># 返回 data.token,后续请求带 Header: Authorization: Bearer &lt;TOKEN&gt;</span>`,
devlist:(b)=>`<span class="c"># 2) 设备列表(带 Token)</span>
curl ${b}/devices \\
-H 'Authorization: Bearer &lt;TOKEN&gt;'`,
cmd:(b)=>`<span class="c"># 3) 发送设备控制指令(开关 / 设置)</span>
curl -X POST ${b}/devices/{deviceId}/commands \\
-H 'Authorization: Bearer &lt;TOKEN&gt;' \\
-H 'Content-Type: application/json' \\
-d '{"deviceId":"{deviceId}","commandType":"switch","params":{"power":"on"}}'`,
ingest:(b)=>`<span class="c"># 4) 设备上报时序数据(iot-data :9082)</span>
curl -X POST ${b}/data/devices/{deviceId}/data \\
-H 'Content-Type: application/json' \\
-d '[{"key":"temperature","type":"number","value":26.5,"timestamp":1697000000000,"quality":"good"}]'`,
scene:(b)=>`<span class="c"># 5) 创建结构化场景(一键执行 + 延时 + 通知)</span>
curl -X POST ${b}/scenes/v2 \\
-H 'Authorization: Bearer &lt;TOKEN&gt;' \\
-H 'Content-Type: application/json' \\
-d '{
"sceneName":"回家模式",
"sceneType":"one_click",
"conditionLogic":"AND",
"conditions":[],
"actions":[
{"type":"set_property","deviceId":"DEV001","propertyCode":"switch_1","value":true},
{"type":"delay","seconds":3},
{"type":"notify","channel":"push","content":"已开启回家模式"}
]
}'`,
rulelist:(b)=>`<span class="c"># 6) 规则列表(按会员隔离,传 Token 看自己的)</span>
curl ${b}/rules \\
-H 'Authorization: Bearer &lt;TOKEN&gt;'`
};
/* ===== 接口 → 模型 映射 ===== */
const EPMAP={
"POST /api/v1/auth/login":{body:"LoginRequest",resp:"LoginResponse"},
"POST /api/v1/auth/register":{body:"RegisterRequest"},
"POST /api/v1/auth/member/login":{body:"LoginRequest"},
"POST /api/v1/auth/member/refresh":{body:"RefreshRequest"},
"POST /api/v1/auth/member/register":{body:"MemberRegisterRequest"},
"PUT /api/v1/auth/member/password":{body:"PasswordChange"},
"GET /api/v1/users":{query:"UserQuery"},
"POST /api/v1/users":{body:"UserRequest"},
"PUT /api/v1/users/{id}":{body:"UserRequest"},
"PUT /api/v1/users/{id}/password":{body:"PasswordChange"},
"GET /api/v1/roles":{query:"RoleQuery"},
"POST /api/v1/roles":{body:"RoleRequest"},
"PUT /api/v1/roles/{id}":{body:"RoleRequest"},
"GET /api/v1/members":{query:"MemberQuery"},
"POST /api/v1/members":{body:"MemberRequest"},
"PUT /api/v1/members/{id}":{body:"MemberRequest"},
"GET /api/v1/logs":{query:"LogQuery"},
"PUT /api/v1/admin/platforms/{platformCode}":{body:"PlatformConfigRequest"},
"GET /api/v1/devices":{query:"DeviceQuery"},
"POST /api/v1/devices":{body:"Device",entity:true},
"PUT /api/v1/devices/{id}":{body:"Device",entity:true},
"POST /api/v1/devices/{deviceId}/connect":{body:"DeviceConnectRequest"},
"POST /api/v1/devices/{deviceId}/commands":{body:"DeviceCommand"},
"POST /api/v1/devices/activities/{deviceId}/activity":{body:"DeviceActivityReport"},
"GET /api/v1/products":{query:"ProductQuery"},
"POST /api/v1/products":{body:"Product",entity:true},
"PUT /api/v1/products/{id}":{body:"Product",entity:true},
"PUT /api/v1/products/{id}/spec":{body:"ProductSpec"},
"POST /api/v1/products/{id}/properties":{body:"ProductProperty",entity:true},
"POST /api/v1/products/{id}/events":{body:"ProductEvent",entity:true},
"POST /api/v1/products/{id}/actions":{body:"ProductAction",entity:true},
"POST /api/v1/products/{id}/firmware":{body:"Firmware",entity:true},
"POST /api/v1/products/{id}/ota":{body:"OtaTaskCreateRequest"},
"POST /api/v1/ota-tasks/{id}/report":{body:"OtaReportRequest"},
"GET /api/v1/device-groups":{query:"GroupQuery"},
"POST /api/v1/device-groups":{body:"DeviceGroup",entity:true},
"PUT /api/v1/device-groups/{id}":{body:"DeviceGroup",entity:true},
"POST /api/v1/homes":{body:"HomeRequest"},
"PUT /api/v1/homes/{id}":{body:"HomeRequest"},
"POST /api/v1/homes/join":{body:"JoinHomeRequest"},
"PUT /api/v1/homes/{id}/members/{memberId}/role":{body:"MemberRoleRequest"},
"POST /api/v1/homes/{homeId}/rooms":{body:"RoomRequest"},
"PUT /api/v1/homes/{homeId}/rooms/{roomId}":{body:"RoomRequest"},
"POST /api/v1/tuya/devices/bind":{body:"TuyaBindRequest"},
"POST /api/v1/tuya/devices/status":{body:"DeviceStatusSyncRequest"},
"POST /api/v1/tuya/auth/bind-native":{body:"TuyaNativeBindRequest"},
"POST /api/v1/tuya/auth/sms/send":{body:"SmsSendRequest"},
"POST /api/v1/tuya/auth/sms/bind":{body:"SmsBindRequest"},
"POST /api/v1/tuya/auth/bind-token":{body:"BindTokenRequest"},
"POST /api/v1/tuya/auth/callback":{body:"OauthCallback"},
"POST /api/v1/tuya/devices/{deviceId}/commands":{body:"TuyaCommand"},
"POST /api/v1/tuya/devices/{deviceId}/rename":{body:"TuyaRename"},
"POST /api/v1/data/devices/{deviceId}/data":{body:"DeviceDataPoint",isArray:true},
"GET /api/v1/data/devices/{deviceId}/history":{query:"DataHistoryQuery"},
"GET /api/v1/data/devices/{deviceId}/stats":{query:"DataStatsQuery"},
"POST /api/v1/rules":{body:"RuleEntity",entity:true},
"PUT /api/v1/rules/{ruleId}":{body:"RuleEntity",entity:true},
"GET /api/v1/rules":{query:"RuleQuery"},
"GET /api/v1/scenes":{query:"SceneQuery"},
"POST /api/v1/scenes":{body:"SceneEntity",entity:true},
"PUT /api/v1/scenes/{sceneId}":{body:"SceneEntity",entity:true},
"POST /api/v1/scenes/v2":{body:"SceneCreateRequest"},
"PUT /api/v1/scenes/{sceneId}/v2":{body:"SceneCreateRequest"},
"POST /api/v1/alarm-rules":{body:"AlarmRuleEntity",entity:true},
"PUT /api/v1/alarm-rules/{ruleId}":{body:"AlarmRuleEntity",entity:true},
"GET /api/v1/alarms":{query:"AlarmQuery"},
"POST /api/v1/ai/command":{body:"AiCommandRequest"},
"POST /api/v1/ai/aliases":{body:"AiAliasRequest"},
"PUT /api/v1/ai/devices/{deviceId}/profile":{body:"AiDeviceProfileRequest"},
/* —— 中控屏(Panel) —— */
"POST /api/v1/panels/session":{body:"PanelSessionReq",resp:"PanelSession"},
"POST /api/v1/panels/session/refresh":{body:"PanelSessionRefreshReq",resp:"PanelSessionRefreshResp"},
"POST /api/v1/panels/{panelId}/heartbeat":{body:"PanelHeartbeat",resp:"PanelHeartbeatResp"},
"GET /api/v1/panels/{panelId}":{resp:"PanelInfo"},
"GET /api/v1/panels/{panelId}/snapshot":{query:"PanelSnapshotQuery",resp:"PanelSnapshot"},
"GET /api/v1/panels/{panelId}/configuration":{resp:"PanelConfig"},
"POST /api/v1/panels/{panelId}/devices/register":{body:"PanelDeviceRegister",resp:"PanelDeviceRegisterResult"},
"PUT /api/v1/panels/{panelId}/devices/{deviceId}/schema":{body:"PanelSchemaUpdate",resp:"PanelSchemaUpdateResult"},
"POST /api/v1/panels/{panelId}/reports":{body:"PanelReportBatch",resp:"PanelReportResult"},
"POST /api/v1/panels/{panelId}/commands/pull":{body:"PanelCmdPullReq",resp:"PanelCmdPullResp"},
"POST /api/v1/panels/{panelId}/commands/{commandId}/start":{body:"PanelCmdStartReq",resp:"PanelCmdStartResp"},
"POST /api/v1/panels/{panelId}/commands/{commandId}/report":{body:"PanelCmdReportReq",resp:"PanelCmdResult"},
"POST /api/v1/panels/{panelId}/diagnostics":{body:"PanelDiagnostic"},
"GET /api/v1/panels/{panelId}/events":{query:"PanelEventQuery",resp:"PanelEventsResp"},
"GET /api/v1/panels/{panelId}/ota-tasks/next":{resp:"PanelOtaNext"},
"POST /api/v1/panels/{panelId}/scene-runs/report":{body:"PanelSceneRunReport",resp:"PanelSceneRunView"},
"POST /api/v1/member/panels/bind":{body:"PanelBindReq",resp:"PanelBindResult"},
"GET /api/v1/member/panels":{resp:"PanelSummary"},
"GET /api/v1/member/panels/bindable":{resp:"PanelBindable"},
"POST /api/v1/admin/panels":{body:"PanelAdminCreate",resp:"PanelAdminCreateResult"},
"GET /api/v1/admin/panels":{resp:"PanelAdminItem"},
"PUT /api/v1/admin/panels/{panelId}/configuration":{body:"PanelConfigSave",resp:"PanelConfig"},
"GET /api/v1/admin/panels/{panelId}/configuration":{resp:"PanelConfig"},
"POST /api/v1/admin/panels/{panelId}/rotate":{resp:"PanelAdminRotateResult"},
"POST /api/v1/admin/panels/executor-reassign":{body:"PanelReassignReq",resp:"PanelReassignResult"}
};
/* ===== 页面与导航 ===== */
const PAGES=[
{id:"index",label:"概览首页",group:"入门"},
{id:"auth",label:"认证与账号",group:"账号体系"},
{id:"member",label:"会员与家庭",group:"账号体系"},
{id:"device",label:"设备管理",group:"设备接入"},
{id:"product",label:"产品与物模型",group:"设备接入"},
{id:"tuya",label:"涂鸦云集成",group:"设备接入"},
{id:"data",label:"数据采集",group:"数据服务"},
{id:"rule",label:"规则与场景",group:"智能联动"},
{id:"ai",label:"AI 智能",group:"智能联动"},
{id:"alarm",label:"告警服务",group:"智能联动"},
{id:"models",label:"数据模型",group:"参考"},
{id:"partner",label:"授权对接",group:"参考"},
{id:"panel",label:"中控屏接口",group:"设备接入"}
];
/* ===== 接口分组(按页面) ===== */
const SECTIONS={};
let _cur=null;
function sec(page,title,sub){_cur={title,sub,items:[]};(SECTIONS[page]=SECTIONS[page]||[]).push(_cur);}
function ep(m,p,d,a,params,ex){_cur.items.push({m,p,d,a,params,ex:ex||""});}
/* —— 认证与账号 auth —— */
sec("auth","认证与登录","后台 / H5 账号登录注册、获取当前用户信息");
ep("POST","/api/v1/auth/login","用户登录,返回 JWT",AUTH.none,"body: LoginRequest","login");
ep("POST","/api/v1/auth/register","注册普通(H5)用户",AUTH.none,"body: RegisterRequest");
ep("GET","/api/v1/auth/info","获取当前登录用户信息",AUTH.req,"Header: Authorization: Bearer <token>");
sec("auth","后台用户管理","用户 CRUD、启用禁用、重置密码、角色分配");
ep("GET","/api/v1/users","后台用户列表(分页)",AUTH.none,"query: UserQuery");
ep("GET","/api/v1/users/{id}","用户详情",AUTH.none,"path: id");
ep("POST","/api/v1/users","创建用户",AUTH.none,"body: UserRequest");
ep("PUT","/api/v1/users/{id}","更新用户",AUTH.none,"path: id; body: UserRequest");
ep("DELETE","/api/v1/users/{id}","删除用户",AUTH.none,"path: id");
ep("PUT","/api/v1/users/{id}/status","启用/禁用用户",AUTH.none,"path: id; query: status(0/1)");
ep("PUT","/api/v1/users/{id}/password","重置用户密码",AUTH.none,"path: id; body: PasswordChange");
ep("GET","/api/v1/users/{id}/roles","用户角色ID列表",AUTH.none,"path: id");
sec("auth","角色与权限","角色 CRUD、角色下拉");
ep("GET","/api/v1/roles","角色列表(分页)",AUTH.none,"query: RoleQuery");
ep("GET","/api/v1/roles/all","全部角色(下拉)",AUTH.none,"-");
ep("GET","/api/v1/roles/{id}","角色详情",AUTH.none,"path: id");
ep("POST","/api/v1/roles","创建角色",AUTH.none,"body: RoleRequest");
ep("PUT","/api/v1/roles/{id}","更新角色",AUTH.none,"path: id; body: RoleRequest");
ep("DELETE","/api/v1/roles/{id}","删除角色",AUTH.none,"path: id");
sec("auth","操作日志","后台操作审计分页");
ep("GET","/api/v1/logs","操作日志分页",AUTH.none,"query: LogQuery");
sec("auth","第三方平台配置","涂鸦 / 大模型等平台凭证配置");
ep("GET","/api/v1/admin/platforms/{platformCode}","获取第三方平台配置",AUTH.none,"path: platformCode(tuya/...)");
ep("PUT","/api/v1/admin/platforms/{platformCode}","更新平台配置",AUTH.none,"path: platformCode; body: PlatformConfigRequest");
ep("POST","/api/v1/admin/platforms/{platformCode}/toggle","启用/禁用平台",AUTH.none,"path: platformCode; query: enabled(bool)");
ep("POST","/api/v1/admin/platforms/{platformCode}/test","测试平台连接",AUTH.none,"path: platformCode");
/* —— 会员与家庭 member —— */
sec("member","会员端账号","移动端会员登录、注册、资料、改密");
ep("POST","/api/v1/auth/member/login","会员(移动端)登录",AUTH.none,"body: LoginRequest");
ep("POST","/api/v1/auth/member/refresh","刷新会员 Token",AUTH.none,"body: RefreshRequest");
ep("POST","/api/v1/auth/member/register","会员注册",AUTH.none,"body: MemberRegisterRequest");
ep("GET","/api/v1/auth/member/info","当前会员信息",AUTH.none,"query: memberId");
ep("PUT","/api/v1/auth/member/profile","更新会员资料",AUTH.none,"query: memberId; form: nickname/avatar/email(可空)");
ep("PUT","/api/v1/auth/member/password","修改会员密码",AUTH.none,"query: memberId; body: PasswordChange");
sec("member","会员管理(后台)","会员 CRUD、启用禁用、重置密码");
ep("GET","/api/v1/members","会员列表(分页)",AUTH.none,"query: MemberQuery");
ep("GET","/api/v1/members/{id}","会员详情",AUTH.none,"path: id");
ep("POST","/api/v1/members","创建会员",AUTH.none,"body: MemberRequest");
ep("PUT","/api/v1/members/{id}","更新会员",AUTH.none,"path: id; body: MemberRequest");
ep("DELETE","/api/v1/members/{id}","删除会员",AUTH.none,"path: id");
ep("PUT","/api/v1/members/{id}/status","启用/禁用会员",AUTH.none,"path: id; query: status(0/1)");
ep("PUT","/api/v1/members/{id}/password","重置会员密码",AUTH.none,"path: id; body: PasswordChange");
sec("member","家庭管理","家庭的创建、成员、分享、加入、离开");
ep("GET","/api/v1/homes","我的家庭列表",AUTH.none,"query: memberId");
ep("GET","/api/v1/homes/{id}","家庭详情",AUTH.none,"path: id; query: memberId");
ep("POST","/api/v1/homes","创建家庭",AUTH.none,"query: memberId; body: HomeRequest");
ep("PUT","/api/v1/homes/{id}","更新家庭",AUTH.none,"path: id; query: memberId; body: HomeRequest");
ep("DELETE","/api/v1/homes/{id}","删除家庭",AUTH.none,"path: id; query: memberId");
ep("POST","/api/v1/homes/{id}/share","生成家庭分享码",AUTH.none,"path: id; query: memberId");
ep("POST","/api/v1/homes/join","加入家庭",AUTH.none,"query: memberId; body: JoinHomeRequest");
ep("GET","/api/v1/homes/{id}/members","家庭成员列表",AUTH.none,"path: id; query: memberId");
ep("PUT","/api/v1/homes/{id}/members/{memberId}/role","修改成员角色",AUTH.none,"path: id,memberId; query: operatorId; body: MemberRoleRequest");
ep("DELETE","/api/v1/homes/{id}/members/{memberId}","移除成员",AUTH.none,"path: id,memberId; query: operatorId");
ep("POST","/api/v1/homes/{id}/leave","离开家庭",AUTH.none,"path: id; query: memberId");
sec("member","房间管理","家庭下房间增删改");
ep("GET","/api/v1/homes/{homeId}/rooms","房间列表",AUTH.none,"path: homeId; query: memberId");
ep("POST","/api/v1/homes/{homeId}/rooms","添加房间",AUTH.none,"path: homeId; query: memberId; body: RoomRequest");
ep("DELETE","/api/v1/homes/{homeId}/rooms/{roomId}","删除房间",AUTH.none,"path: homeId,roomId; query: memberId");
ep("PUT","/api/v1/homes/{homeId}/rooms/{roomId}","重命名房间",AUTH.none,"path: homeId,roomId; query: memberId; body: RoomRequest");
/* —— 设备管理 device —— */
sec("device","设备管理","设备 CRUD、连接/断开、控制指令、控制模型、父网关、操控动态");
ep("GET","/api/v1/devices","获取设备列表",AUTH.opt,"query: DeviceQuery","devlist");
ep("GET","/api/v1/devices/{deviceId}","设备详情",AUTH.none,"path: deviceId");
ep("POST","/api/v1/devices","创建设备",AUTH.none,"body: Device");
ep("PUT","/api/v1/devices/{id}","更新设备",AUTH.none,"path: id; body: Device");
ep("DELETE","/api/v1/devices/{id}","删除设备",AUTH.none,"path: id");
ep("GET","/api/v1/devices/{deviceId}/control-schema","设备标准控制模型",AUTH.none,"path: deviceId");
ep("GET","/api/v1/devices/{deviceId}/data","读取设备数据",AUTH.none,"path: deviceId");
ep("POST","/api/v1/devices/{deviceId}/connect","连接设备",AUTH.none,"path: deviceId; body: DeviceConnectRequest");
ep("POST","/api/v1/devices/{deviceId}/disconnect","断开设备",AUTH.none,"path: deviceId");
ep("POST","/api/v1/devices/{deviceId}/commands","发送控制指令",AUTH.none,"path: deviceId; body: DeviceCommand","cmd");
ep("POST","/api/v1/devices/{deviceId}/parent","绑定/解绑父网关",AUTH.none,"path: deviceId; body: {parentDeviceId} (空=解绑)");
ep("GET","/api/v1/devices/activities/recent","近期操控动态(首页)",AUTH.opt,"query: homeId, limit(默认5)");
ep("GET","/api/v1/devices/activities","动态分页查询",AUTH.opt,"query: homeId, deviceId, pageNum, pageSize");
ep("POST","/api/v1/devices/activities/{deviceId}/activity","上报设备控制动态",AUTH.none,"path: deviceId; body: DeviceActivityReport");
sec("device","设备分组","分组树 / 分页 / CRUD / 设备计数");
ep("GET","/api/v1/device-groups/tree","分组树形列表",AUTH.none,"-");
ep("GET","/api/v1/device-groups","分组分页",AUTH.none,"query: GroupQuery");
ep("POST","/api/v1/device-groups","创建分组",AUTH.none,"body: DeviceGroup");
ep("PUT","/api/v1/device-groups/{id}","更新分组",AUTH.none,"path: id; body: DeviceGroup");
ep("DELETE","/api/v1/device-groups/{id}","删除分组",AUTH.none,"path: id");
ep("GET","/api/v1/device-groups/{id}/devices/count","分组下设备数量",AUTH.none,"path: id");
/* —— 产品与物模型 product —— */
sec("product","产品管理","产品 CRUD、按平台解析、存量归类");
ep("GET","/api/v1/products","产品列表",AUTH.none,"query: ProductQuery");
ep("GET","/api/v1/products/{id}","产品详情",AUTH.none,"path: id");
ep("POST","/api/v1/products","创建产品",AUTH.none,"body: Product");
ep("PUT","/api/v1/products/{id}","更新产品",AUTH.none,"path: id; body: Product");
ep("DELETE","/api/v1/products/{id}","删除产品",AUTH.none,"path: id");
ep("GET","/api/v1/products/resolve","按平台信息解析产品",AUTH.none,"query: platform,platformProductId,model,manufacturer,category,name");
ep("POST","/api/v1/products/reconcile","存量设备按型号归类",AUTH.none,"-");
ep("POST","/api/v1/products/reconcile-spec","补全省物模型",AUTH.none,"query: memberId");
sec("product","物模型(TSL)","产品标准控制模型、属性/事件/服务维护");
ep("GET","/api/v1/products/{id}/spec","物模型(属性/事件/服务)",AUTH.none,"path: id");
ep("GET","/api/v1/products/{id}/control-schema","产品标准控制模型",AUTH.none,"path: id");
ep("PUT","/api/v1/products/{id}/spec","整体保存物模型",AUTH.none,"path: id; body: ProductSpec");
ep("POST","/api/v1/products/{id}/properties","新增/更新属性(DP)",AUTH.none,"path: id; body: ProductProperty");
ep("POST","/api/v1/products/{id}/events","新增/更新事件",AUTH.none,"path: id; body: ProductEvent");
ep("POST","/api/v1/products/{id}/actions","新增/更新服务",AUTH.none,"path: id; body: ProductAction");
ep("DELETE","/api/v1/spec/{type}/{id}","删除物模型项",AUTH.none,"path: type(property/event/action), id");
sec("product","固件与 OTA","固件注册/发布、OTA 任务、回滚、统计");
ep("POST","/api/v1/products/{id}/firmware","注册固件(草稿)",AUTH.none,"path: id; body: Firmware");
ep("GET","/api/v1/products/{id}/firmwares","产品固件分页",AUTH.none,"path: id; query: page,size");
ep("GET","/api/v1/products/{id}/firmware/latest","最新已发布固件",AUTH.none,"path: id");
ep("PUT","/api/v1/firmwares/{id}/publish","发布固件",AUTH.none,"path: id");
ep("POST","/api/v1/products/{id}/ota","创建OTA任务",AUTH.none,"path: id; body: OtaTaskCreateRequest");
ep("GET","/api/v1/ota-tasks","OTA任务分页",AUTH.none,"query: page,size,productId");
ep("GET","/api/v1/ota-tasks/{id}","OTA任务详情",AUTH.none,"path: id");
ep("POST","/api/v1/ota-tasks/{id}/report","设备上报升级结果",AUTH.none,"path: id; body: OtaReportRequest");
ep("POST","/api/v1/ota-tasks/{id}/cancel","暂停/取消任务",AUTH.none,"path: id");
ep("POST","/api/v1/products/{id}/ota/rollback","版本回滚",AUTH.none,"path: id; query: targetVersion(可空)");
ep("GET","/api/v1/ota-tasks/stats","OTA统计看板",AUTH.none,"query: productId(可空)");
/* —— 涂鸦云集成 tuya —— */
sec("tuya","账号绑定","OAuth 授权、Token/原生/短信绑定、解绑");
ep("GET","/api/v1/tuya/auth/url","获取涂鸦 OAuth 授权URL",AUTH.req,"Header: Bearer <token>");
ep("GET","/api/v1/tuya/auth/status","涂鸦绑定状态",AUTH.req,"Header: Bearer <token>");
ep("POST","/api/v1/tuya/auth/callback","处理OAuth回调",AUTH.req,"body: OauthCallback");
ep("GET","/api/v1/tuya/auth/callback-page","OAuth回调页(返回HTML)",AUTH.none,"query: code, state");
ep("POST","/api/v1/tuya/auth/bind-token","Token绑定涂鸦账号",AUTH.req,"body: BindTokenRequest");
ep("POST","/api/v1/tuya/auth/auto-bind","自动绑定涂鸦账号",AUTH.req,"Header: Bearer <token>");
ep("POST","/api/v1/tuya/auth/bind-native","APP原生SDK绑定",AUTH.req,"body: TuyaNativeBindRequest");
ep("POST","/api/v1/tuya/auth/unbind","解绑涂鸦账号",AUTH.req,"Header: Bearer <token>");
ep("POST","/api/v1/tuya/auth/sms/send","发送绑定验证码",AUTH.none,"body: SmsSendRequest");
ep("POST","/api/v1/tuya/auth/sms/bind","验证码绑定涂鸦账号",AUTH.req,"body: SmsBindRequest");
sec("tuya","设备与场景","涂鸦家庭/设备/场景同步与控制");
ep("GET","/api/v1/tuya/homes","涂鸦家庭列表",AUTH.req,"Header: Bearer <token>");
ep("GET","/api/v1/tuya/devices","涂鸦设备列表",AUTH.req,"query: homeId(可空)");
ep("POST","/api/v1/tuya/devices/bind","绑定涂鸦设备到平台",AUTH.req,"body: TuyaBindRequest");
ep("POST","/api/v1/tuya/devices/status","设备状态实时同步",AUTH.none,"body: DeviceStatusSyncRequest");
ep("POST","/api/v1/tuya/devices/{deviceId}/commands","发送涂鸦控制指令",AUTH.req,"path: deviceId; body: 涂鸦指令数组 List<{code,value}>");
ep("GET","/api/v1/tuya/scenes","涂鸦场景列表",AUTH.req,"query: homeId(可空)");
ep("POST","/api/v1/tuya/scenes/{sceneId}/trigger","执行涂鸦场景",AUTH.req,"path: sceneId");
ep("GET","/api/v1/tuya/devices/{deviceId}/status","涂鸦设备状态",AUTH.req,"path: deviceId");
ep("GET","/api/v1/tuya/devices/{deviceId}/cloud-status","涂鸦云端在线状态",AUTH.req,"path: deviceId");
ep("GET","/api/v1/tuya/subscribe/status","消息订阅状态",AUTH.none,"-");
sec("tuya","AI 指令码","设备 AI 指令码档案探测");
ep("GET","/api/v1/tuya/devices/{deviceId}/ai-profile","设备AI指令码档案",AUTH.req,"path: deviceId");
ep("POST","/api/v1/tuya/devices/{deviceId}/ai-profile/detect","探测AI指令码档案",AUTH.req,"path: deviceId; query: force");
ep("POST","/api/v1/tuya/ai-profile/detect-all","批量探测AI指令码",AUTH.req,"Header: Bearer <token>");
sec("tuya","设备与配网","重命名、Wi-Fi/Matter/Zigbee 配网 Token");
ep("POST","/api/v1/tuya/devices/{deviceId}/rename","重命名涂鸦设备",AUTH.req,"path: deviceId; body: TuyaRename");
ep("POST","/api/v1/tuya/config/wifi-token","Wi-Fi配网Token",AUTH.req,"query: homeId(默认0)");
ep("POST","/api/v1/tuya/config/matter-token","Matter配网Token",AUTH.req,"query: homeId(默认0)");
ep("POST","/api/v1/tuya/config/zigbee-token","Zigbee配网Token",AUTH.req,"query: homeId(默认0)");
sec("tuya","天气","当前天气查询");
ep("GET","/api/v1/weather","获取当前天气",AUTH.none,"query: lat(默认31.23), lon(默认121.47)");
/* —— 数据采集 data —— */
sec("data","时序数据","设备数据点上报、历史查询、聚合统计、最新值");
ep("POST","/api/v1/data/devices/{deviceId}/data","接收设备上报数据",AUTH.none,"path: deviceId; body: List<DeviceDataPoint>","ingest");
ep("GET","/api/v1/data/devices/{deviceId}/history","查询历史数据",AUTH.none,"path: deviceId; query: DataHistoryQuery");
ep("GET","/api/v1/data/devices/{deviceId}/stats","数据聚合统计",AUTH.none,"path: deviceId; query: DataStatsQuery");
ep("GET","/api/v1/data/devices/{deviceId}/latest","获取最新数据",AUTH.none,"path: deviceId");
/* —— 规则与场景 rule —— */
sec("rule","规则引擎","规则 CRUD、启停、手动执行、设备触发");
ep("GET","/api/v1/rules","规则列表",AUTH.opt,"query: RuleQuery","rulelist");
ep("GET","/api/v1/rules/{ruleId}","规则详情",AUTH.none,"path: ruleId");
ep("POST","/api/v1/rules","创建规则",AUTH.opt,"body: RuleEntity");
ep("PUT","/api/v1/rules/{ruleId}","更新规则",AUTH.none,"path: ruleId; body: RuleEntity");
ep("DELETE","/api/v1/rules/{ruleId}","删除规则",AUTH.none,"path: ruleId");
ep("POST","/api/v1/rules/{ruleId}/enable","启用规则",AUTH.none,"path: ruleId");
ep("POST","/api/v1/rules/{ruleId}/disable","禁用规则",AUTH.none,"path: ruleId");
ep("POST","/api/v1/rules/{ruleId}/execute","手动执行规则",AUTH.none,"path: ruleId; body: Object(触发数据)");
ep("POST","/api/v1/rules/trigger/{deviceId}","触发设备数据变化",AUTH.none,"path: deviceId; body: Object");
sec("rule","场景联动","场景 CRUD、启停、一键执行、定时(cron)、结构化场景");
ep("GET","/api/v1/scenes","场景分页列表",AUTH.opt,"query: SceneQuery");
ep("GET","/api/v1/scenes/{sceneId}","场景详情",AUTH.none,"path: sceneId");
ep("POST","/api/v1/scenes","创建场景",AUTH.opt,"body: SceneEntity");
ep("PUT","/api/v1/scenes/{sceneId}","更新场景",AUTH.none,"path: sceneId; body: SceneEntity");
ep("DELETE","/api/v1/scenes/{sceneId}","删除场景",AUTH.none,"path: sceneId");
ep("POST","/api/v1/scenes/{sceneId}/enable","启用场景",AUTH.none,"path: sceneId");
ep("POST","/api/v1/scenes/{sceneId}/disable","禁用场景",AUTH.none,"path: sceneId");
ep("POST","/api/v1/scenes/{sceneId}/execute","一键执行场景",AUTH.none,"path: sceneId");
ep("PUT","/api/v1/scenes/{sceneId}/cron","设置定时执行(cron)",AUTH.none,"path: sceneId; body: {cron} (六段式 0 0 * * * ?)");
ep("POST","/api/v1/scenes/v2","创建结构化场景",AUTH.opt,"body: SceneCreateRequest","scene");
ep("PUT","/api/v1/scenes/{sceneId}/v2","更新结构化场景",AUTH.opt,"path: sceneId; body: SceneCreateRequest");
ep("GET","/api/v1/scenes/{sceneId}/v2","结构化场景详情",AUTH.none,"path: sceneId");
/* —— AI 智能 ai —— */
sec("ai","语音控制","自然语言指令、设备指令码、AI 日志");
ep("POST","/api/v1/ai/command","执行自然语言控制指令",AUTH.req,"body: AiCommandRequest");
ep("GET","/api/v1/ai/devices","可AI控制设备清单",AUTH.req,"Header: Bearer <token>");
ep("GET","/api/v1/ai/devices/{deviceId}/profile","查询设备控制指令码",AUTH.req,"path: deviceId");
ep("PUT","/api/v1/ai/devices/{deviceId}/profile","校准设备控制指令码",AUTH.req,"path: deviceId; body: AiDeviceProfileRequest");
ep("GET","/api/v1/ai/logs","最近AI指令日志",AUTH.req,"query: limit(默认20)");
sec("ai","别名管理","设备/场景/房间别名的增删查、语音鉴权");
ep("GET","/api/v1/ai/aliases","查询设备/场景/房间别名",AUTH.req,"query: targetType(device/scene/room)");
ep("POST","/api/v1/ai/aliases","新增/更新别名",AUTH.req,"body: AiAliasRequest");
ep("DELETE","/api/v1/ai/aliases/{aliasId}","删除别名",AUTH.req,"path: aliasId");
ep("GET","/api/v1/ai/speech/auth","讯飞语音听写鉴权地址",AUTH.req,"Header: Bearer <token>");
/* —— 告警服务 alarm —— */
sec("alarm","告警","告警列表/详情/确认/解决/统计");
ep("GET","/api/v1/alarms","告警列表",AUTH.opt,"query: AlarmQuery");
ep("GET","/api/v1/alarms/{alarmId}","告警详情",AUTH.none,"path: alarmId");
ep("POST","/api/v1/alarms/{alarmId}/confirm","确认告警",AUTH.none,"path: alarmId; body: {userId}");
ep("POST","/api/v1/alarms/{alarmId}/resolve","解决告警",AUTH.none,"path: alarmId; body: {userId}");
ep("GET","/api/v1/alarms/stats","告警统计",AUTH.none,"-");
sec("alarm","告警规则","告警规则 CRUD");
ep("GET","/api/v1/alarm-rules","告警规则列表",AUTH.none,"query: pageNum,pageSize");
ep("POST","/api/v1/alarm-rules","创建告警规则",AUTH.none,"body: AlarmRuleEntity");
ep("PUT","/api/v1/alarm-rules/{ruleId}","更新告警规则",AUTH.none,"path: ruleId; body: AlarmRuleEntity");
ep("DELETE","/api/v1/alarm-rules/{ruleId}","删除告警规则",AUTH.none,"path: ruleId");
/* —— 中控屏接口 panel —— */
sec("panel","屏端 · 身份与会话","中控屏(面板)专用身份层,与会员/后台隔离;凭证建会话后持 panel JWT 调用其余接口");
ep("POST","/api/v1/panels/session","建立会话:终端初始化凭证换受限身份",AUTH.none,"Header: Authorization: PanelBootstrap <bootstrapSecret>; body: PanelSessionReq");
ep("POST","/api/v1/panels/session/refresh","刷新会话:轮换刷新凭证",AUTH.none,"body: PanelSessionRefreshReq");
ep("POST","/api/v1/panels/{panelId}/heartbeat","屏心跳:续在线租期",AUTH.req,"body: PanelHeartbeat");
ep("GET","/api/v1/panels/{panelId}","屏信息",AUTH.req,"path: panelId");
sec("panel","屏端 · 家庭快照","首屏拉取家庭/房间/设备完整视图(设备支持游标分页)");
ep("GET","/api/v1/panels/{panelId}/snapshot","家庭快照(含 rooms/devices/stateEpoch)",AUTH.req,"query: PanelSnapshotQuery");
sec("panel","屏端 · 配置","运营配置拉取(带 checksum,屏端比对是否变更)");
ep("GET","/api/v1/panels/{panelId}/configuration","拉取屏运营配置",AUTH.req,"path: panelId");
sec("panel","屏端 · 设备登记与物模型","本地设备登记、上报标准控制模型");
ep("POST","/api/v1/panels/{panelId}/devices/register","登记本地设备",AUTH.req,"body: PanelDeviceRegister");
ep("PUT","/api/v1/panels/{panelId}/devices/{deviceId}/schema","更新设备标准控制模型",AUTH.req,"path: panelId,deviceId; body: PanelSchemaUpdate");
sec("panel","屏端 · 状态上报","批量设备状态上报(按 epoch/sequence 去旧)");
ep("POST","/api/v1/panels/{panelId}/reports","批量设备状态上报",AUTH.req,"body: PanelReportBatch");
sec("panel","屏端 · 命令执行","拉取 → 申请 → 回报 三段式命令流");
ep("POST","/api/v1/panels/{panelId}/commands/pull","拉取待执行命令(QUEUED→DELIVERED)",AUTH.req,"body: PanelCmdPullReq");
ep("POST","/api/v1/panels/{panelId}/commands/{commandId}/start","申请执行(DELIVERED→EXECUTING)",AUTH.req,"path: panelId,commandId; body: PanelCmdStartReq");
ep("POST","/api/v1/panels/{panelId}/commands/{commandId}/report","命令执行回报",AUTH.req,"path: panelId,commandId; body: PanelCmdReportReq");
sec("panel","屏端 · 诊断与 OTA","诊断上报、屏端 OTA 升级任务拉取");
ep("POST","/api/v1/panels/{panelId}/diagnostics","上报诊断事件(network/mqtt/ota/command/config)",AUTH.req,"body: PanelDiagnostic");
ep("GET","/api/v1/panels/{panelId}/ota-tasks/next","拉取下一个待升级任务",AUTH.req,"path: panelId");
ep("POST","/api/v1/panels/{panelId}/scene-runs/report","屏上报本地设备场景执行结果",AUTH.req,"body: PanelSceneRunReport");
sec("panel","屏端 · 实时事件推送","长轮询 + SSE 双通道(非 WebSocket);屏端二选一,~20s 心跳");
ep("GET","/api/v1/panels/{panelId}/events","长轮询家庭增量事件(阻塞~20s)",AUTH.req,"query: PanelEventQuery");
ep("GET","/api/v1/panels/{panelId}/events/stream","SSE 增量事件流(text/event-stream,非 Result 包装)",AUTH.req,"query: cursor");
sec("panel","会员侧 · 自助绑定(APP 扫码)","与屏身份层隔离;memberId 取自会员 JWT 不信任参数");
ep("GET","/api/v1/member/panels/bindable","可绑定性预检(是否已预置/启用/已绑)",AUTH.req,"query: serialNumber");
ep("POST","/api/v1/member/panels/bind","扫码绑定:预置屏绑到自己的家庭",AUTH.req,"body: PanelBindReq");
ep("GET","/api/v1/member/panels","我的屏列表",AUTH.req,"-");
ep("POST","/api/v1/member/panels/{panelId}/unbind","解绑(仅绑定者本人,释放家庭)",AUTH.req,"path: panelId");
sec("panel","后台 · 屏终端管理","ROLE_ADMIN;预置屏终端、启用禁用、轮换凭证、配置、归属交接");
ep("POST","/api/v1/admin/panels","预置屏终端(一次性初始化凭证)",AUTH.req,"body: PanelAdminCreate");
ep("GET","/api/v1/admin/panels","屏终端列表",AUTH.req,"-");
ep("PUT","/api/v1/admin/panels/{panelId}/status","启用/禁用屏终端",AUTH.req,"path: panelId; query: status(0/1)");
ep("POST","/api/v1/admin/panels/{panelId}/rotate","轮换终端初始化凭证",AUTH.req,"path: panelId");
ep("GET","/api/v1/admin/panels/{panelId}/configuration","查看屏配置",AUTH.req,"path: panelId");
ep("PUT","/api/v1/admin/panels/{panelId}/configuration","保存屏配置",AUTH.req,"path: panelId; body: PanelConfigSave");
ep("POST","/api/v1/admin/panels/executor-reassign","多屏归属交接(routeRevision 递增)",AUTH.req,"body: PanelReassignReq");
/* ===== 渲染工具 ===== */
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function jsonHTML(obj){return esc(JSON.stringify(obj,null,2));}
function fieldTable(fields){
let t='<table class="ft"><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody>';
fields.forEach(f=>{
const rc=f[2]==='是'?'rq':(f[2]==='否'?'op':'');
t+=`<tr><td><code>${esc(f[0])}</code></td><td><span class="ty">${esc(f[1])}</span></td><td class="${rc}">${esc(f[2])}</td><td>${esc(f[3])}</td></tr>`;
});
return t+'</tbody></table>';
}
function modelCard(key){
const m=MODELS[key]; if(!m) return '';
let h=`<div class="model" id="model-${key}"><h3>${esc(m.title)}</h3>`;
const tags=[];
if(m.entity) tags.push('<span class="q">实体(entity)</span>');
if(m.query) tags.push('<span class="q">查询参数</span>');
if(m.resp) tags.push('<span class="q">响应结构</span>');
h+=`<div class="mtag">${tags.join(' ')}</div>`;
h+=fieldTable(m.fields);
if(m.nested&&m.nested.length){h+='<div class="resp-note">嵌套对象字段:</div>'+fieldTable(m.nested);}
if(m.entity){h+=`<div class="baseblk"><b>+ 公共基类字段(所有 entity 响应均含)</b>${fieldTable(BASE_FIELDS)}</div>`;}
if(m.example!==undefined){h+=`<details class="exjson"><summary>JSON 示例</summary><pre>${jsonHTML(m.example)}</pre></details>`;}
if(m.sample){h+=`<div class="resp-note">示例查询串:<code class="inl">?${esc(m.sample)}</code></div>`;}
return h+'</div>';
}
function epHTML(e){
const map=EPMAP[e.m+' '+e.p]||{};
let extra='';
if(map.body){
const mk=map.body;
extra+=`<div class="mlink">📦 请求体字段 → <a href="models.html#model-${mk}">${esc(mk)}</a>${map.entity?'(实体,响应含公共基类字段)':''}</div>`;
let j=MODELS[mk]?jsonHTML(MODELS[mk].example):'';
if(map.isArray) j='[\n '+j.replace(/\n/g,'\n ')+'\n]';
extra+=`<details class="ex"><summary>查看请求体 JSON 示例${map.isArray?'(数组)':''}</summary><pre>${j}</pre></details>`;
}
if(map.query){
const qk=map.query;
extra+=`<div class="mlink">🔍 查询参数 → <a href="models.html#model-${qk}">${esc(qk)}</a></div>`;
extra+=`<details class="ex"><summary>查看查询参数表</summary>${fieldTable(MODELS[qk].fields)}${MODELS[qk].sample?('<div class="resp-note">示例:<code class="inl">?'+esc(MODELS[qk].sample)+'</code></div>'):''}</details>`;
}
if(map.resp){extra+=`<div class="mlink">📤 响应结构 → <a href="models.html#model-${map.resp}">${esc(map.resp)}</a></div>`;}
const exBlock=e.ex?`<details class="ex"><summary>查看请求示例(curl)</summary><pre data-ex="${e.ex}"></pre></details>`:'';
const st=(e.m+' '+e.p+' '+e.d+' '+e.params+' '+e.a+' '+(map.body||'')+' '+(map.query||'')).toLowerCase();
const tc=e.a===AUTH.req?'t-req':e.a===AUTH.opt?'t-opt':'t-none';
return `<div class="ep" data-text="${esc(st)}"><div class="row"><span class="badge m-${e.m}">${e.m}</span><span class="path">${esc(e.p)}</span><span class="tag ${tc}">${e.a}</span></div><div class="desc">${esc(e.d)}</div><div class="params"><b>参数:</b>${esc(e.params)}</div>${extra}${exBlock}</div>`;
}
function renderSections(pageId){
const secs=SECTIONS[pageId]||[];
let h='';
if(secs.length>1){h+='<div class="toc">';secs.forEach((s,i)=>{h+=`<a href="#g-${pageId}-${i}">${esc(s.title)}</a>`;});h+='</div>';}
secs.forEach((s,i)=>{
h+=`<section class="sec" id="g-${pageId}-${i}"><h2 class="h2">${esc(s.title)}</h2><div class="sub">${esc(s.sub)}</div>`;
s.items.forEach(e=>{h+=epHTML(e);});
h+='</section>';
});
return h;
}
function renderModels(){
let h='<div class="toc">';Object.keys(MODELS).forEach(k=>{h+=`<a href="#model-${k}">${esc(k)}</a>`;});h+='</div>';
Object.keys(MODELS).forEach(k=>{h+=modelCard(k);});
h+=`<div class="baseblk" style="margin-top:10px"><b>统一响应包装 Result&lt;T&gt;</b>:<code class="inl">{ "code": 0, "message": "success", "data": T }</code>。分页接口 data 即 <a href="#model-PageResult">PageResult</a>;单条增改查 data 为对应 entity(如 <a href="#model-Device">Device</a>)。</div>`;
return h;
}
function totalEndpoints(){let n=0;for(const k in SECTIONS)n+=SECTIONS[k].reduce((a,s)=>a+s.items.length,0);return n;}
function renderOverview(){
let h=`<div class="ov-hero"><h1>${PRODUCT_NAME} 平台 · REST API 文档</h1><p>统一入口 <code class="inl">/api/v1</code> · 前后端分离 · JWT Bearer 鉴权 · 含字段级类型与 JSON 示例<br>共收录 ${totalEndpoints()} 个接口 / ${Object.keys(MODELS).length} 个数据模型,按业务域拆分多页。</p></div>`;
h+='<div class="ov-cards">';
PAGES.forEach(p=>{
if(p.id==='index')return;
const cnt=p.id==='models'?Object.keys(MODELS).length:(SECTIONS[p.id]?SECTIONS[p.id].reduce((a,s)=>a+s.items.length,0):0);
const descMap={auth:'后台登录注册、用户/角色/日志/平台配置',member:'会员账号、家庭与房间',device:'设备 CRUD、连接、控制指令、分组',product:'产品、物模型 TSL、固件与 OTA',tuya:'涂鸦 OAuth 绑定、设备/场景控制、配网',data:'时序数据上报、历史、聚合',rule:'规则引擎与场景联动(含 cron)',ai:'自然语言控制与语音',alarm:'告警与告警规则',models:'全部请求/响应字段与 JSON 示例',partner:'开放平台授权对接:应用凭证 ak/sk 换取 JWT,后台应用管理、接入指南与接口说明',panel:'中控屏(面板)身份、会话、快照、命令、实时事件推送、OTA 与会员自助绑定'};
h+=`<a class="ov-card" href="${p.id}.html"><h3>${esc(p.label)}</h3><p>${descMap[p.id]||''}</p><span class="cnt">${cnt} 项 →</span></a>`;
});
h+='</div>';
h+=`<div class="ov-note"><b>鉴权说明:</b>需登录的接口在请求头携带 <code class="inl">Authorization: Bearer &lt;token&gt;</code>。后台管理类代码层不强制校验,由网关/反代保证;标注 <b style="color:var(--req)">强制</b> 的接口需会员 Token(无效即报错);<b style="color:var(--opt)">可选</b> 仅用于数据隔离(会员看自己的、admin 看全部);<b style="color:var(--none)">无</b> 不校验。<br><b>统一响应:</b><code class="inl">{"code":0,"message":"success","data":{...}}</code>;分页 data 为 <code class="inl">{records,total,pages,current,size}</code>。所有 <code class="inl">entity</code> 响应额外含公共基类字段(见「数据模型」章末)。</div>`;
return h;
}
function getBase(){try{return localStorage.getItem('apiBase')||DEFAULT_BASE;}catch(e){return DEFAULT_BASE;}}
function setBase(v){try{localStorage.setItem('apiBase',v);}catch(e){}renderExamples(v);}
function renderExamples(b){document.querySelectorAll('#content .ex pre[data-ex]').forEach(pre=>{const k=pre.dataset.ex;if(EXAMPLES[k])pre.innerHTML=EXAMPLES[k](b);});}
function filterPage(){const q=(document.getElementById('q').value||'').trim().toLowerCase();let vis=0;document.querySelectorAll('#content .ep').forEach(el=>{const ok=!q||el.dataset.text.indexOf(q)>=0;el.classList.toggle('hidden',!ok);if(ok)vis++;});const em=document.getElementById('empty');if(em)em.style.display=vis?'none':'block';}
function renderSidebar(cur){
let h=`<div class="brand"><h2>${PRODUCT_NAME} · API</h2><p>REST 接口文档 v1</p></div>`;
let last=null;
PAGES.forEach(p=>{if(p.group!==last){h+=`<div class="grp">${esc(p.group)}</div>`;last=p.group;}h+=`<a class="nav${p.id===cur?' active':''}" href="${p.id}.html">${esc(p.label)}</a>`;});
h+='<div class="ft">由代码扫描自动汇总<br>若有出入以服务端代码为准</div>';
document.getElementById('sidebar').innerHTML=h;
}
function renderTopbar(pageId){
const p=PAGES.find(x=>x.id===pageId);
let h=`<div class="ptitle">${esc(p.label)}</div>`;
h+=`<input class="search" id="q" placeholder="🔍 搜索本页接口 / 模型" oninput="filterPage()">`;
h+=`<div class="basebox">Base URL <input id="base" value="${esc(getBase())}" oninput="setBase(this.value)"></div>`;
document.getElementById('topbar').innerHTML=h;
}
function renderPartner(){
const B=getBase();
return `
<h1>开放平台 · 授权对接(接入指南)</h1>
<p class="ov-note">本平台采用 <b>应用凭证(ak/sk) + 换取 JWT</b> 的授权模式,与会员 JWT 同源校验。合作伙伴先在后台创建应用拿到 <code class="inl">AppKey / AppSecret</code>,再用 Secret 换取短期 <code class="inl">Access Token</code>,最后以 <code class="inl">Bearer</code> 方式调用业务接口。</p>
<h2>一、名词说明</h2>
<ul>
<li><b>AppKey</b>:公开标识,形如 <code class="inl">ak-xxxx</code>,可安全出现在前端/配置中。</li>
<li><b>AppSecret</b>:密钥,形如 <code class="inl">sk-xxxx</code>,<b>必须保密</b>;服务端仅存储其 BCrypt 哈希,创建/轮换时明文仅展示一次。</li>
<li><b>Access Token</b>:换取得到的 JWT(默认有效期 7 天),用于调用业务接口。</li>
</ul>
<h2>二、对接流程</h2>
<ol>
<li>后台 <b>系统管理 → 应用管理 → 新建应用</b>,生成 ak/sk(Secret 仅弹窗展示一次,请立即保存)。</li>
<li>调用 <code class="inl">POST /api/v1/auth/token</code>,用 ak/sk 换取 Access Token。</li>
<li>业务接口请求头带上 <code class="inl">Authorization: Bearer &lt;accessToken&gt;</code>。</li>
<li>密钥泄露或人员变动时,在后台 <b>轮换密钥</b>(旧 Secret 立即失效)或 <b>禁用</b> 应用。</li>
</ol>
<h2>三、换取 Token</h2>
<p>支持两种传参方式(JSON 或 OAuth2 client_credentials 表单):</p>
<details class="ex"><summary>查看 curl 示例(JSON)</summary><pre>curl -X POST '${B}/api/v1/auth/token' \\
-H 'Content-Type: application/json' \\
-d '{"appKey":"ak-xxxxxxxxxxxx","appSecret":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'</pre></details>
<details class="ex"><summary>查看 curl 示例(client_credentials 表单)</summary><pre>curl -X POST '${B}/api/v1/auth/token' \\
-H 'Content-Type: application/x-www-form-urlencoded' \\
-d 'grant_type=client_credentials&client_id=ak-xxxxxxxxxxxx&client_secret=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'</pre></details>
<p>返回:<code class="inl">{"accessToken":"eyJ...","tokenType":"Bearer","expiresIn":604800,"appKey":"ak-xxx"}</code></p>
<h2>四、调用业务接口</h2>
<details class="ex"><summary>查看 curl 示例(携带 Token)</summary><pre>curl '${B}/api/v1/devices?pageNum=1&pageSize=10' \\
-H 'Authorization: Bearer &lt;accessToken&gt;'</pre></details>
<p class="ov-note">应用 Token 拥有 <code class="inl">ROLE_APP</code> 权限,可访问业务接口;建议结合后台配置的 <b>授权范围(scopes)</b>、<b>IP 白名单</b>、<b>限流</b> 与 <b>过期时间</b> 做最小权限控制。</p>
<h2>五、后台管理接口(管理员)</h2>
<ul>
<li><code class="inl">POST /api/v1/admin/apps</code> 创建应用(返回 ak/sk)</li>
<li><code class="inl">GET /api/v1/admin/apps</code> 应用列表</li>
<li><code class="inl">GET /api/v1/admin/apps/{id}</code> 应用详情</li>
<li><code class="inl">PUT /api/v1/admin/apps/{id}/status?status=0|1</code> 启用/禁用</li>
<li><code class="inl">POST /api/v1/admin/apps/{id}/rotate</code> 轮换密钥(返回新 sk)</li>
</ul>
<h2>六、安全建议</h2>
<ul>
<li><b>全链路 HTTPS</b>:Token 与 Secret 均为明文承载,务必通过 HTTPS 传输。</li>
<li><b>Secret 不出服务端</b>:服务端仅存 BCrypt 哈希,前端/日志不落明文。</li>
<li><b>最小权限</b>:为每个对接方配置 scopes / IP 白名单 / 限流 / 有效期。</li>
<li><b>可吊销</b>:发生泄露立即「禁用」或「轮换密钥」,无需改代码。</li>
</ul>
`;
}
function initDoc(pageId){
renderSidebar(pageId);
renderTopbar(pageId);
let c='';
if(pageId==='index')c=renderOverview();
else if(pageId==='models')c=renderModels();
else if(pageId==='partner')c=renderPartner();
else c=renderSections(pageId);
c+='<div class="empty" id="empty">本页没有匹配的接口</div>';
c+=`<footer>文档由代码扫描自动汇总 · 共 ${totalEndpoints()} 个接口 / ${Object.keys(MODELS).length} 个数据模型 · 若有出入以服务端代码为准</footer>`;
document.getElementById('content').innerHTML=c;
renderExamples(getBase());
}
</script>
<script>initDoc('panel');</script>
</body>
</html>