你的第一次受治理变更

你手上已经有一个跑起来的 AuraBoot,里面装着 Showcase 插件——快速开始已经帮你导入了。从这里再花十分钟,你会改掉这个插件、亲眼看到平台拒绝写入你没有声明过的东西、认识每次写入都要通过的两道门,然后读到你刚才所有动作的审计记录。

一次坐下来,把整个产品看完。这里没有一步是玩具——真实功能走的就是这条路。

开始之前 —— 先完成 快速开始。你需要一个已经初始化、并且导入了插件的栈,而且能登录进去。

下面每条命令都假设你已经有了一个管理员 token:

BACKEND=http://localhost:3000   # 浏览器地址;对外只发布这一个端口
ADMIN_EMAIL=[email protected]  # use the account configured during bootstrap
read -r -s -p "Administrator password: " ADMIN_PASSWORD; printf '\n'
JWT=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL="$ADMIN_EMAIL" \
  ADMIN_PASSWORD="$ADMIN_PASSWORD" \
  python3 scripts/application/core-admin-session.py)

在克隆的 AuraBoot 仓库根目录执行,所用版本必须包含 scripts/application/core-admin-session.py。helper 登录后通过真实租户选择 API 选中唯一业务空间,再返回该空间 token。初始化管理员也可能属于平台空间,因此不能直接拿裸登录 token 执行业务命令。若有多个业务空间,显式设置 AURA_ADMIN_TENANT_ID 为目标业务租户。不要公开 token 或密码。

1. 看看你已经有了什么

创建一条 Showcase 记录,再给它记一条月度指标:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_showcase \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_name":"Verify Widget","sc_description":"my first governed change",
                  "sc_quantity":10,"sc_price":99.5},"operationType":"CREATE"}'
{"code":"0","data":{"commandCode":"sc:create_showcase",
                    "phaseReached":"completed",
                    "data":{"recordPid":"01M3GK5TCVTRVQH66H8BS0ZE8E",
                            "showcase_all_fields_inserted":1},
                    "executionTimeMs":133}}

记下 data.data.recordPid——后面的月度指标挂在它上面。列表、列、表单全部来自声明文件——plugins/showcase/config/models.json,以及它旁边的 fields、bindings、commands。这是第一个观念:模型就是 Git 里的一个文件,UI 是从它派生出来的。(同样的页面在 字段展示 → 全字段类型 里也能点出来,不喜欢 curl 就用界面。)

再记一条月度指标:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<你的 recordPid>","sc_month":9,
                  "sc_revenue":1200,"sc_cost":800,"sc_units":40,
                  "sc_metric_remark":"baseline"},"operationType":"CREATE"}'

它能跑通,而没有任何人写过这个页面或这个接口。

2. 加一个字段

销售想在每个月的实际上报旁边记录一个目标值。打开 plugins/showcase/config/fields/sc_monthly_metric.json,追加:

{
  "code": "sc_metric_target",
  "displayName:en": "Target",
  "displayName:zh-CN": "目标",
  "dataType": "decimal",
  "constraints": { "required": false },
  "feature": { "sortable": true }
}

存在的一个字段不等于模型在用的字段。还要绑定它——plugins/showcase/config/bindings/sc_monthly_metric.json:

{
  "modelCode": "sc_monthly_metric",
  "fieldCode": "sc_metric_target",
  "sequence": 7,
  "required": false,
  "visible": true,
  "editable": true,
  "displayConfig": { "sortable": true }
}

重新导入插件,让平台读到你的文件。path 是后端看到的插件目录——Docker 栈里是 /app/plugins/showcase;后端跑在宿主机上时,是 <仓库>/plugins/showcase:

curl -s -X POST $BACKEND/api/plugins/import/import-directory-sync \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"path":"/app/plugins/showcase","conflictStrategy":"OVERWRITE"}'

导入声明后,模型元数据发生变化:

  • 数据库变了。 mt_sc_monthly_metric 多了一列 sc_metric_target numeric。你没写任何 migration。
  • 字段绑定到 sc_monthly_metric。 它不会加到另一个模型 showcase_all_fields 的表单中。本教程通过指标命令与持久化行验证;要在自定义页面展示它,还需显式增加页面字段声明。

3. 看平台拒绝写入它

带着 target 建一条指标:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<你的 recordPid>","sc_month":8,
                  "sc_revenue":900,"sc_cost":700,"sc_units":30,
                  "sc_metric_target":777},"operationType":"CREATE"}'

命令报告成功——"code":"0"、"phaseReached":"completed"。看一眼那行数据:

SELECT sc_month, sc_revenue, sc_metric_target
FROM   mt_sc_monthly_metric ORDER BY created_at DESC LIMIT 1;
     8 | 900.00 | (空)

目标值没了。 不报错。不警告。直接丢掉。

这是第二个观念,也是大多数平台做错的那个。字段在模型上存在,但 sc:create_monthly_metric 从没声明过它会接受它。打开 plugins/showcase/config/commands/sc_monthly_metric.json:

{
  "code": "sc:create_monthly_metric",
  "type": "create",
  "inputFields": [
    "sc_showcase_pid", "sc_month", "sc_revenue",
    "sc_cost", "sc_units", "sc_metric_remark"
  ],
  "permissions": ["sc.showcase.manage"],
  ...
}

inputFields 是白名单,不是文档。不在名单上的字段不会进入持久化映射。它不校验已声明字段的取值范围;例如 sc_month 是否允许 13,由字段约束与命令校验负责。

把 "sc_metric_target" 加进 inputFields,重新导入,再建一次:

curl -s -X POST "$BACKEND/api/meta/commands/execute/sc:create_monthly_metric" \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<你的 recordPid>","sc_month":7,
                  "sc_revenue":1500,"sc_cost":700,"sc_units":30,
                  "sc_metric_target":777},"operationType":"CREATE"}'

使用新的月份,避免触发同一展示记录与月份的唯一约束。核对本次返回的指标 recordPid 对应行:

     7 | 1500.00 | 777.00

两个声明,两份工作。模型说这个东西是什么;命令说谁能写什么、系统自己填什么。

4. 被拒绝——在两道不同的门上

读规则和亲身体会被拒不是一回事。造两个同事,看平台对其中一个说不。

# Sam:租户基线角色,没有任何业务授权。
curl -s -X POST $BACKEND/api/meta/commands/execute/admin:create_member \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"name":"Sam Reader","email":"[email protected]",
                  "password":"Test2026x","roleCodes":["tenant_member"]},
       "operationType":"CREATE"}'

# Rita:展示模块管理员。
curl -s -X POST $BACKEND/api/meta/commands/execute/admin:create_member \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"name":"Rita Sales","email":"[email protected]",
                  "password":"Test2026x","roleCodes":["sc_admin"]},
       "operationType":"CREATE"}'

分别以他们的身份登录,执行同一条命令、同一个载荷:

SAM=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL=[email protected] \
  ADMIN_PASSWORD=Test2026x python3 scripts/application/core-admin-session.py)
RITA=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL=[email protected] \
  ADMIN_PASSWORD=Test2026x python3 scripts/application/core-admin-session.py)

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $SAM" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<你的 recordPid>","sc_month":6,
                  "sc_revenue":100,"sc_cost":50,"sc_units":5},
       "operationType":"CREATE"}'
curl -s -X POST "$BACKEND/api/meta/commands/execute/sc:create_monthly_metric" \
  -H "Authorization: Bearer $RITA" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<你的 recordPid>","sc_month":6,
                  "sc_revenue":100,"sc_cost":50,"sc_units":5},
       "operationType":"CREATE"}'
Sam — tenant_member403 Access forbidden
Rita — sc_admin"code":"0",数据入库

同一条命令。同一个载荷。差别只在角色。

进来这一路上你其实穿过了两道门,值得把名字都记住:

  1. 端点门。 所有写入都汇聚到同一个地方:/api/meta/commands/execute/...。这个端点本身要求 meta.command.execute。Sam 就是在这里被拒的——报错的 context 会点名缺的那个权限。如果你造一个只有业务角色、没有执行授权的成员,会看到同样的拒绝;默认拒绝意味着一个角色能过这道门,一定是因为有人授过权(Showcase 插件在 plugins/showcase/config/roles.json 里把它授给了 sc_admin)。
  2. 命令门。 过了端点,命令自己的声明开始生效:"permissions": ["sc.showcase.manage"]。这一行就写在你在第 3 步编辑的那个文件里。

两道门都不是绕得过去的 UI 级检查。这个端点是唯一的写入路径。点保存的人、自动化、BPM 节点、调用工具的 AI agent——四种身份都从这里进来,都撞上同样这两行。

5. 读你刚才做过什么

每一个过了门的执行都会留下记录。

SELECT command_code, success, phase_reached, execution_time_ms
FROM   ab_command_audit_log
ORDER  BY created_at DESC
LIMIT  6;
 sc:create_monthly_metric | t | completion | 22
 admin:create_member      | t | completion | 109
 sc:create_monthly_metric | t | completion | 35

管线接受了什么、花了多久、走到了哪一步。你没有打开过这个开关。你也没法只给某一个调用方关掉它,因为调用方只有一个——管线。

Sam 的 403 不在里面。 端点门在管线运行之前就把他拒了,管线的审计日志根本看不见这次尝试。去找之前先知道这件事:今天这张表回答的是「做了什么」,不是「试图做什么」。

你刚刚证明了什么

模型是一个文件你声明并绑定了字段,导入后增加数据库列;页面字段仍需显式声明。
命令是一份契约它默默拒绝了一个从未答应接受的值——并在审计日志里如实交代它收下了什么。
写入要过两道门端点的 meta.command.execute,然后是命令自己的权限。都是声明,不是按钮。
审计不是一个功能它是「入口只有一条」这个事实的必然结果——对每一个进得来的东西而言。

下一步

  • 创建你的第一个模型 —— 同样的闭环,只是这次由你来写模型。
  • 权限 —— 五层权限,以及一个角色是怎么够到一条命令的。
  • 命令管线 —— 你刚在 phase_reached 里看到的那些阶段。
  • 搭建 CRM —— 七章走完,最后发布一个模块。