你的第一次受治理变更
你手上已经有一个跑起来的 AuraBoot,里面装着一套 CRM。从这里再花十分钟,你会改掉这套 CRM、亲眼看到平台拒绝写入你没有声明过的东西,然后读到你刚才所有动作的审计记录。
一次坐下来,把整个产品看完。这里没有一步是玩具——真实功能走的就是这条路。
开始之前 —— 先完成 快速开始。你需要一个已经初始化、并且导入了插件的栈,而且能登录进去。
下面所有命令都假设你有一个管理员 token:
BACKEND=http://localhost:3000 # 你在浏览器里打开的那个地址;只有这个端口被映射出来
JWT=$(curl -s -X POST $BACKEND/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"Test2026x"}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["jwt"])')1. 先看看你已经有了什么
打开 CRM → 线索,从界面上新建一条。能用 —— 而这个页面没有任何人写过。
列表、列、表单、校验,全部来自一份声明:plugins/crm-starter/config/fields/crm_lead.json,加上一份 binding(说明这个模型实际用到哪些字段)。这是第一个理念:模型是 Git 里的一个文件,界面是从它推导出来的。
2. 加一个字段
销售想记录预算。打开 plugins/crm-starter/config/fields/crm_lead.json,追加:
{
"code": "crm_lead_budget",
"displayName:en": "Budget",
"displayName:zh-CN": "预算",
"dataType": "decimal",
"constraints": { "required": false },
"feature": { "sortable": true }
}字段存在,不等于模型在用它。 到 plugins/crm-starter/config/bindings/crm_lead.json 里绑上:
{
"modelCode": "crm_lead",
"fieldCode": "crm_lead_budget",
"sequence": 15,
"required": false,
"visible": true,
"editable": true,
"displayConfig": { "sortable": true }
}重新导入插件:
curl -s -X POST $BACKEND/api/plugins/import/import-directory-sync \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{"path":"/app/plugins/crm-starter","conflictStrategy":"OVERWRITE"}'两件事在你没要求的情况下发生了:
- 数据库变了。
mt_crm_lead多了一列crm_lead_budget numeric。你没写任何 migration。 - 界面变了。 刷新 CRM → 线索,表单里已经有"预算"。
3. 看着平台拒绝写入它
带预算新建一条线索:
curl -s -X POST $BACKEND/api/meta/commands/execute/crm:create_lead \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{"payload":{"crm_lead_company":"Contoso Ltd",
"crm_lead_contact_name":"Bo Chen",
"crm_lead_budget":50000},
"operationType":"CREATE"}'命令报告成功 —— "code":"0"、"phaseReached":"completion"。然后看那一行数据:
LEAD-20260713-002 | Contoso Ltd | budget = (空)预算没了。 不是报错,不是告警,是被丢弃。
这是第二个理念,也是多数平台做错的地方。字段确实在模型上,但 crm:create_lead 从来没说过它接受这个字段。打开 plugins/crm-starter/config/commands/crm_lead.json:
{
"code": "crm:create_lead",
"type": "create",
"inputFields": [
"crm_lead_company", "crm_lead_contact_name", "crm_lead_contact_phone",
"crm_lead_contact_email", "crm_lead_source", "crm_lead_industry",
"crm_lead_score", "crm_lead_campaign", "owner_type", "owner_id",
"crm_lead_requirement"
],
"permissions": ["crm.lead.manage"],
"autoSetFields": {
"crm_lead_code": { "strategy": "auto_generate", "pattern": "LEAD-{yyyyMMdd}-{seq}" },
"crm_lead_status": { "strategy": "fixed_value", "value": "new" }
}
}inputFields 是白名单,不是文档。不在上面的一律丢弃 —— 而这正是你希望发生的事,比如哪天有人往你的 API POST 一个 {"crm_lead_status":"won"}。也留意 autoSetFields:crm_lead_code 是自动生成的,所以客户端无论传什么都定不了它。
把 "crm_lead_budget" 加进 inputFields,重新导入,再建一次:
LEAD-20260713-003 | Fabrikam Inc | budget = 50000.00两份声明,两件事。模型说明这个东西是什么;命令说明什么可以被写、谁可以写、以及系统自己填什么。
4. 注意谁被允许
其实你刚才已经看到了,就在同一条命令上:
"permissions": ["crm.lead.manage"]这不是一个界面层的检查、绕过去就没了。它是命令自己的契约,在管道里强制执行 —— 而管道是唯一的写入路径。一个人点保存、一个自动化、一个 BPM 节点、一个调用工具的 AI agent:四者都会来到这里,都会被同一行代码衡量。
没有第二条路。整个设计的要点就在这。
5. 读一遍你做过的事
每一次执行 —— 无论通过还是被拒 —— 都留下记录。
SELECT command_code, success, phase_reached, execution_time_ms, request_payload
FROM ab_command_audit_log
ORDER BY created_at DESC
LIMIT 3;crm:create_lead | t | completion | 24ms | {"crm_lead_code": "LEAD-20260713-003",
"crm_lead_budget": 50000,
"crm_lead_status": "new",
"crm_lead_company": "Fabrikam Inc", …}
crm:create_lead | t | completion | 87ms | …
crm:create_lead | t | completion | 532ms | …管道真正接受的 payload、谁发的、耗时多久、走到了哪一阶段。你没有打开过这个开关;你也没法只对某一个调用方关掉它 —— 因为根本只有一个调用方:管道。
你刚刚证明了什么
| 模型是一个文件 | 你加了个字段,列和表单跟着就有了。没有 migration,没有页面。 |
| 命令是一份契约 | 它静默拒绝了自己从未同意接受的值 —— 并在审计里如实记下它真正接受了什么。 |
| 权限挂在命令上 | 不是挂在按钮上。人、自动化、agent 都过同一行。 |
| 审计不是一个功能 | 它是"只有一条入口"这件事的必然结果。 |
下一步
- 创建第一个 Model —— 同样的闭环,但这次是你从零写模型,而不是改一个现成的。
- Permission —— 五层权限,以及一个角色是怎么够到一条命令的。
- Command Pipeline —— 你刚才在
phase_reached里看到的那些阶段。 - 搭建 CRM —— 七章走完,最后发布一个模块。