项目:运营搭建平台(vue3-course) | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇给出 10 个核心接口的完整请求/响应示例,覆盖登录、看板草稿的保存与读取、三阶段发布、数据源查询代理、数据库连接等高频契约。示例 JSON 依据类型定义(packages/shared/src/schema/index.ts、packages/work-server/src/types.ts)与 controller/service 的实际字段构造,凡非真实值一律为示意,标注方式:字段语义说明写在注解里,值本身可读出示意的(如 uuid 片段、时间戳)不再逐一打标。通用约定先复述一遍(详见《03-接口设计》第 1 节与《15-接口全量参考》第 0 节):写与动作走 /api/post/*、读走 /api/get/*;响应统一 { data, success, message }(packages/work-server/src/types.ts 的 MyAPIResult);可预期业务失败一律 HTTP 200 + success:false,401/403/404/429 只表达传输层事实;登录态是 HttpOnly cookie online-status(curl 场景用 -c/-b 维持 cookie jar)。示例统一假设管理端在 http://127.0.0.1:8001、门户在 http://127.0.0.1:6001。
响应包络是统一的。两个服务端的所有业务接口都返回 { data, success, message } 三键结构,类型定义在 packages/work-server/src/types.ts 的 MyAPIResult。这意味着调用方的错误处理可以完全模板化:先判 HTTP 状态码(只可能是 200/401/403/404/429/503 六种),再判 success 布尔,最后把 message 直接展示给用户——它被 internalFailure 收敛过,永远是用户可读文案而非技术细节。data 在失败时通常是 null(查询类接口给空数组),调用方不要依赖"失败时 data 的部分字段"这种约定,它不存在。
字段命名是"库下划线、接口驼峰"。数据库列是 modify_time/create_time/owner_uuid/schema_json(下划线蛇形),接口层统一下发驼峰 modifyTime/createTime/ownerUuid/schemaJson——转换发生在 controller/service 层,不是序列化库的自动行为,所以个别历史接口可能存在漏改(⚠️ 待确认:以 15 篇逐端点表格为准)。写请求同理:前端传 schemaJson,落库进 schema_json 列。
认证是 cookie 而非 header。除 portal 的取数通道外,全部接口的登录态是 HttpOnly cookie online-status,没有任何 Bearer token 或自定义鉴权头的入口。这决定了两件事:跨域部署必须解决 cookie 的 SameSite/secure 问题(而不是改用 header 令牌);服务间调用(portal → work-server)不共享登录态,靠的是独立的 x-internal-token 共享密钥——它的语义是"我是内网通道"而不是"我是某个用户",终点裁决完全依赖 prod 白名单。
时间与软删是隐式契约。所有主表都有 create_time(DEFAULT CURRENT_TIMESTAMP)与 modify_time(ON UPDATE 自动更新),接口层只读不可写;删除一律是软删(status=0),回收站恢复把 status 置回 1——发布状态不随恢复回滚(restore 只动草稿行)。写用例或脚本时不要假设 DELETE 之后行不存在,它只是不可见。
分页与列表是固定形状。列表端点统一接受 pageNum/pageSize(默认 10,packages/work-server/src/util/pagination.ts 的 clampPage 钳制非法值与上限,四条线共用同一个上限),响应统一为 { list, total, pageNum, pageSize, keyword? };看板列表另有 visibility 过滤与 scope=all|mine("全部可见 | 我拥有的"切换)。分页是兜底而非优化——列表行可能带大字段(schema 全文),见 10 篇第 3 节的量级警告。
请求(IP 桶限流 20 次/10 分钟,packages/work-server/src/router.ts 153 行注册点):
curl -s -c /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/account/sign-in \
-H 'Content-Type: application/json' \
-H 'Origin: http://127.0.0.1:8001' \
-d '{"username":"admin001","password":"88888888"}'成功响应:
{
"data": { "username": "admin001", "uuid": "u-1001 ※ 示意" },
"success": true,
"message": "登录成功"
}同时 Set-Cookie 下发 online-status(HS256 JWT,HttpOnly + SameSite=Lax,TTL 1 小时;curl 已由 -c /tmp/cookies.txt 接住)。失败响应(HTTP 仍 200):
{ "data": null, "success": false, "message": "登录失败,用户名称或密码错误" }要点:① 本项目带 cookie 的 POST 必须带同源 Origin(或 Sec-Fetch-Site),否则被 sameSiteGuard 以 403「跨站请求已被拒绝」拦截——curl 加 -H 'Origin: ...' 即可通过(08 篇第 8 节);② 失败文案已回源核实(packages/work-server/src/service/user.ts 45/58 行):无此用户与密码错误统一返回「登录失败,用户名称或密码错误」,参数缺任一项也是同文案——这是防用户枚举的常规做法;③ 401 只出现在未登录访问受保护端点时,登录接口本身不返回 401。
curl -s -b /tmp/cookies.txt http://127.0.0.1:8001/api/get/account/online成功响应(已登录):
{
"data": {
"username": "admin001",
"uuid": "u-1001 ※ 示意",
"roles": ["admin"],
"permissions": ["dashboard:create", "dashboard:update", "dashboard:publish", "datasource:query", "… ※ admin 全量权限码,完整矩阵见 insert_role_permission_admin.sql"]
},
"success": true,
"message": "成功"
}未登录响应(HTTP 200,不报错):
{ "data": { "username": null, "uuid": null }, "success": true, "message": "成功" }要点:未登录不抛 401 而是返回空用户——前端路由守卫据此决定放行或跳登录页(packages/work-front/src/router.ts)。权限码实时查库(四表联查),JWT payload 不含权限,所以改角色后无需重新登录即可在此接口反映。
权限码 dashboard:create。请求:
curl -s -b /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/dashboard/create \
-H 'Content-Type: application/json' -H 'Origin: http://127.0.0.1:8001' \
-d '{
"name": "双十一大促监控",
"description": "大促当日实时监控看板",
"visibility": "private",
"schemaJson": { "components": [], "state": {}, "methods": {}, "layoutMode": "flow", "canvas": { "background": "", "guides": "none" } }
}'成功响应:
{
"data": {
"uuid": "9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b ※ 示意",
"name": "双十一大促监控"
},
"success": true,
"message": "创建成功"
}错误响应(HTTP 200):
{ "data": null, "success": false, "message": "名称不能为空" }要点:name 经 normalizeResourceName(packages/work-server/src/util/sanitize.ts)trim + 剔除控制字符,超 128 字符被挡在边界(MySQL 1406 → "系统繁忙"的前置防线);schemaJson 传对象(controller 层序列化落库),空 components 即可建板;owner_uuid 取当前登录用户,visibility 非法值落 private(normalizeVisibility)。
权限码 dashboard:update + 仅所有者。这是编辑器 Ctrl+S 的端点,全量上送(无部分更新语义)。请求体核心是 schemaJson,其结构见本篇附录的完整示例:
curl -s -b /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/dashboard/update \
-H 'Content-Type: application/json' -H 'Origin: http://127.0.0.1:8001' \
-d '{
"uuid": "9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"name": "双十一大促监控",
"description": "大促当日实时监控看板",
"visibility": "public",
"schemaJson": { "components": [ "…完整结构见附录…" ], "state": { "loading": false }, "methods": { "refresh": "state.loading = true" }, "layoutMode": "flow", "canvas": { "background": "#0b1220", "guides": "none" } }
}'成功响应:
{ "data": { "uuid": "9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b" }, "success": true, "message": "保存成功" }失败响应(非所有者,两层文案见 08 篇第 5 节):
{ "data": null, "success": false, "message": "暂无权限访问该看板" }要点:保存成功后服务端自动打 source='save' 快照(保留 30 条,失败不阻断保存,packages/work-server/src/service/dashboard-snapshot.ts);改名应走独立的 /api/post/dashboard/rename(防并发旧 schema 覆盖,router 注释);切勿用读到的旧详情整体回写——update 是全量覆盖语义。
权限码 dashboard:view + 可见权。请求与响应:
curl -s -b /tmp/cookies.txt \
"http://127.0.0.1:8001/api/get/dashboard/detail?uuid=9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b"{
"data": {
"uuid": "9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"name": "双十一大促监控",
"description": "大促当日实时监控看板",
"schemaJson": { "components": [ "…见附录…" ], "state": {}, "methods": {}, "layoutMode": "flow", "canvas": {} },
"ownerUuid": "u-1001 ※ 示意",
"visibility": "public",
"tenantId": null,
"departmentId": null,
"isTemplate": 0,
"templateCategory": "",
"accessMode": "public",
"status": 1,
"createTime": "2026-10-01 09:30:00 ※ 示意",
"modifyTime": "2026-10-06 11:20:00 ※ 示意"
},
"success": true,
"message": "成功"
}要点:① 响应不含 accessToken(detail 剥离,owner 专属接口 /api/get/dashboard/access-mode 才返回令牌,08 篇第 6 节);② schemaJson 已 parse 为对象下发(编辑器直接消费);③ accessMode 默认 public(库默认值)——敏感看板发布前务必切 token(12 篇健康检查第 5 条)。
权限码 dashboard:publish + 仅所有者。以 prod 为例:
curl -s -b /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/dashboard/stage/push \
-H 'Content-Type: application/json' -H 'Origin: http://127.0.0.1:8001' \
-d '{"uuid":"9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b","stage":"prod"}'成功响应:
{
"data": { "stage": "prod", "uuid": "9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b", "currentVersion": 3 },
"success": true,
"message": "成功发布到环境prod(v3)"
}三个典型失败响应(HTTP 200,文案即原因):
{ "data": null, "success": false, "message": "请先发布到预发环境" }
{ "data": null, "success": false, "message": "该看板含 2 个页面方法(refresh、track),页面方法会以脚本形式在访客浏览器执行,在独立沙箱上线前不允许公开访问;请改为「令牌访问」,或先移除方法体。" }
{ "data": null, "success": false, "message": "发布到环境prod失败" }要点:① test 阶段从组件 dataBinding 收集数据源 id 冻结白名单 {id,name,type} 进 publishedDataSources,config 永不进产物(packages/work-server/src/service/dashboard-stage.ts 42-52 行);② pre 复制 test、prod 复制 pre,跳阶段被拒;③ prod 在事务内行锁自增版本并自动打 发布 v{n} 快照,重复点击产生连续版本不重号;④ 发布后又给看板绑了新数据源的,必须重走 test → pre → prod 才会进白名单(12 篇第 5 节高频条目)。公开闸门文案已回源核实(packages/work-server/src/util/page-methods-guard.ts 71-76 行):方法名列举最多 5 个(超出加"等"),上文第二条中的"2 个/refresh、track"为示意值。配套查询:GET /api/get/dashboard/stage/status?uuid=… 返回 { data: { test: {currentVersion, modifyTime}, pre: null, prod: {currentVersion, modifyTime} }, success: true }(未发布的阶段为 null)。
权限码 datasource:query + 读权(可见范围判定)。请求:
curl -s -b /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/datasource/query \
-H 'Content-Type: application/json' -H 'Origin: http://127.0.0.1:8001' \
-d '{
"dataSourceId": "ds-88a1 ※ 示意",
"queryParams": { "start": "2026-10-01", "end": "2026-10-07" }
}'成功响应(normalizeData 归一化后的行数组):
{
"data": [
{ "date": "2026-10-01", "pv": 12000, "uv": 4500 },
{ "date": "2026-10-02", "pv": 13500, "uv": 5100 }
],
"success": true,
"message": "查询成功"
}失败响应三种典型:
{ "data": [], "success": false, "message": "SQL 缺少查询参数: start" }
{ "data": [], "success": false, "message": "当前部署禁止 API 数据源访问内网地址" }
{ "data": [], "success": false, "message": "系统繁忙,请稍后再试" }要点:① queryParams 经 buildParameterizedQuery 转驱动绑定值,{{name}} 占位符缺参报错且不留残留占位符(packages/work-server/src/util/data-query/shared.ts);② "系统繁忙"意味着真实错误在服务端 stdout(internalFailure 收敛过,12 篇总原则);③ 该端点最终进入 executeDataSourceQuery——全仓唯一解密出口(08 篇第 7 节),调用方拿不到任何连接配置。
匿名可达(限流 300 次/min/IP + 令牌门)。这是 portal 转发 work-server 的访客取数入口:
curl -s -X POST http://127.0.0.1:6001/api/portal/dashboard/query \
-H 'Content-Type: application/json' \
-d '{"uuid":"9f1c2a34-5b6d-4e7f-8a9b-0c1d2e3f4a5b","dataSourceId":"ds-88a1"}'成功响应(结构同第 7 节,但参数不可传——queryParams 在终点写死空对象):
{ "data": [ { "date": "2026-10-01", "pv": 12000, "uv": 4500 } ], "success": true, "message": "查询成功" }失败响应(按拦截层排列):
{ "data": null, "success": false, "message": "缺少或无效的访问令牌" }
{ "data": null, "success": false, "message": "数据源不在该看板发布白名单内" }
{ "data": null, "success": false, "message": "数据查询通道未配置(INTERNAL_TOKEN)" }
{ "data": null, "success": false, "message": "数据查询暂不可用,请稍后重试" }要点:① 令牌模式下 cookie(portal_access_<uuid>)或 URL ?t= 任一通道通过即放行(packages/portal-server/src/util/access-gate.ts,curl 可加 -d 后再带 ?t=<token> 验证免 cookie 通道);② 四条失败文案分别对应令牌门、双层白名单校验、通道未配置(fail-closed)、work-server 库层故障(该文案刻意中性——响应要出公网,18 篇 2.3 节);③ 访客对数据源的全部能力就是"uuid + dataSourceId"两个参数,其余一切(连接、SQL、参数)都在服务端固化。
权限码 dbconnection:create。SQL 数据源的前置(README 明确约束:数据源存 connectionId,不存 host/port):
curl -s -b /tmp/cookies.txt -X POST http://127.0.0.1:8001/api/post/dbconnection/create \
-H 'Content-Type: application/json' -H 'Origin: http://127.0.0.1:8001' \
-d '{
"name": "业务主库-生产",
"type": "mysql",
"host": "10.0.0.11 ※ 示意(内网部署时须 DATA_QUERY_DENY_PRIVATE=false 放行)",
"port": 3306,
"user": "report_ro",
"password": "plain-secret ※ 示意:明文提交,服务端加密落库",
"database": "biz",
"visibility": "private"
}'成功响应:
{ "data": { "uuid": "conn-3b2a ※ 示意" }, "success": true, "message": "创建成功" }要点:① password 落库前经 encryptSecret 加密为 enc:v1: 前缀密文(AES-256-GCM,packages/work-server/src/util/secret-box.ts),任何读接口都不会再返回它(detail/list 不含口令,08 篇第 7 节);② "测试连接"端点 /api/post/dbconnection/test 会真实建连一次即断;③ 改口令后注意连接池缓存(按 host:port:user:database 缓存,需重启进程生效,12 篇第 5 节)。
CSV 上传 POST /api/post/datasource/upload(multipart,权限码 datasource:create):curl -b /tmp/cookies.txt -F "file=@data.csv" http://127.0.0.1:8001/api/post/datasource/upload,响应 { "data": { "fileId": "… ※ 示意" }, "success": true },fileId 随后填进 CSV 数据源的 config(04 篇 4.2 节)。出码下载 POST /api/post/codegen(登录 + 用户桶配额 30/min + 100/日):请求 { "uuid": "…" },响应是 zip 二进制(Content-Disposition 附件),curl 加 -o dashboard.zip 接住;数据源只带 500 行内快照(resolveCodegenSnapshot),失败回落 mockData 并只落布尔标记。
下面是一份可直接通过 /api/post/dashboard/update 落库、能被三端(编辑画布/访客渲染器/出码器)消费的最小 schemaJson——一个容器 + 一个按钮 + 一个带数据绑定与显隐条件的指标卡,覆盖 ComponentInstance 的全部常用字段(类型依据 packages/shared/src/schema/index.ts):
{
"components": [
{
"id": "c1a2b3c4 ※ 示意:uuid-v4",
"type": "ElCard",
"label": "卡片容器",
"x": 0, "y": 0, "w": 12, "h": 6,
"props": { "shadow": "always" },
"style": { "background": "#111827", "borderRadius": "8px" },
"children": [
{
"id": "d5e6f7a8 ※ 示意",
"type": "NativeHeading",
"label": "标题",
"x": 0, "y": 0, "w": 12, "h": 1,
"props": { "text": "实时监控", "level": "h3" },
"style": { "color": "#f9fafb" }
},
{
"id": "e9f0a1b2 ※ 示意",
"type": "MetricCard",
"label": "PV 指标",
"x": 0, "y": 0, "w": 6, "h": 3,
"props": { "title": "今日 PV", "value": "—", "precision": 0 },
"visibleWhen": { "stateKey": "showPv", "not": false },
"dataBinding": {
"dataSourceId": "ds-88a1 ※ 示意",
"fieldMap": { "xField": "date", "yField": "pv" }
},
"events": { "click": "refresh" }
},
{
"id": "f3c4d5e6 ※ 示意",
"type": "ElButton",
"label": "刷新按钮",
"x": 0, "y": 0, "w": 4, "h": 3,
"props": { "text": "刷新", "type": "primary" },
"events": { "click": "refresh" }
}
]
}
],
"state": { "showPv": true, "loading": false },
"methods": {
"refresh": "state.loading = true; await api.reload('ds-88a1'); state.loading = false ※ 示意:方法体为作者 JS,AsyncFunction(state, methods) 编译"
},
"layoutMode": "flow",
"canvas": { "background": "#0b1220", "guides": "none" }
}发布到 test 后,产物中的同一份 schema 会多出以下字段(草稿中不存在):
{
"publishedDataSources": [
{ "id": "ds-88a1 ※ 示意", "name": "大促 PV 来源", "type": "mysql" }
],
"accessMode": "public"
}字段语义速查(详表见 15 篇第 4 节):components 顶层组件进栅格,容器子组件走 children 文档流纵排;x/y/w/h 在 flow 模式被忽略(旧栅格单位,h 换算 min-height)、absolute 模式为像素坐标;props 纯 JSON 不求值;style 仅 18 键白名单(COMPONENT_STYLE_KEYS,渲染端 sanitizeComponentStyle 再过滤);visibleWhen.stateKey 指向 state 变量,not 取反;events 的值必须是 methods 里声明的方法名,且组件元表 meta.events 声明过该事件;publishedDataSources 由服务端在 test 发布时注入,人工伪造会在 prod 白名单校验处失效;methods 的方法体签名是 (state, methods)(方法互调传自身集合,packages/shared/src/runtime.ts)。
把本篇各接口出现过的错误响应归拢成一张心智图,排查时先按文案归层再往下查——这与 12 篇第 3 节的决策树一致,但这里是契约视角。第一层是认证层(401):requirePermission 与 filterLoginStatus 在登录态缺失/过期时返回「暂无权限,请先登录」,cookie 存在仍 401 通常是服务器时间漂移或 JWT_SECRET 变更。第二层是横切层(403):sameSiteGuard 的「跨站请求已被拒绝(同源校验未通过)」与限流的 429(带 Retry-After 头)——它们与权限无关,任何端点都可能命中。第三层是权限码层(403):「缺少权限: {code}」的 message 会回显具体权限码,直接对照种子矩阵查角色绑定。第四层是资产层(HTTP 200 + success:false):归属(「只有所有者可以…」)与可见性(「暂无权限访问该看板」)两层文案刻意不同——前者说明资源对你存在但不归你写,后者连存在性都不泄露(08 篇第 5 节)。第五层是业务规则层(HTTP 200 + success:false):三阶段的「请先发布到…」、白名单的「数据源不在该看板发布白名单内」、公开闸门的方法体提示——文案即规则本身。第六层是服务与网络层(404/503/200+中性文案):portal 的 404 友好页、库层故障的 503 页、转发链路的「数据查询暂不可用」——这一层刻意模糊,因为响应可能出公网。联调时遇到"系统繁忙"级别的文案,正确的下一步永远是查服务端 stdout 的模块前缀日志,而不是重试。
拿本篇示例联调时按序自检:① cookie jar 是否带上(401 先查 online-status);② POST 是否带同源 Origin(403 跨站文案即此);③ 权限码是否与角色匹配(403「缺少权限: xxx」——自助注册默认 viewer,只读);④ 写操作是否是所有者(403「只有所有者可以…」);⑤ 访客链路三道门顺序——令牌门 → prod 白名单 → queryParams 固定空,任何一道失败文案见第 8 节;⑥ 时间字段以服务端 modify_time(ON UPDATE 自动维护)为准,接口层驼峰下发。更多端点的参数级速查见《15-接口全量参考》,鉴权模型见《08-安全与权限设计》。