service.json 配置详解
这篇文档说明 Service Tree 的 service.json 怎么写。读完后,可以把底层数据集、SQL 或 Backend Function 封装成业务命令,例如:
lovrabet crm customer list
lovrabet crm customer detail 10001
lovrabet crm customer contact list --customer-id 10001新的推荐写法是树状结构:用 resources 表示业务对象,用 actions 表示这个对象上能做什么。旧的 commands 数组仍可用,但只建议在兼容老配置时保留。
先看最小结构
一个服务配置通常包含四部分:
service:服务入口,例如crm。app/apps:服务默认使用哪个应用。resources:服务里有哪些业务对象,例如客户、联系人、跟进记录。actions:每个业务对象能做什么,例如列表、详情、创建。
最小示例:
{
"service": "crm",
"name": "CRM",
"description": "客户、联系人和跟进记录的业务服务",
"app": "app-xxxxxxxx",
"resources": {
"customer": {
"name": "客户",
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actions": {
"list": {
"description": "查看客户列表",
"defaults": {
"currentPage": 1,
"pageSize": 20,
"orderBy": [{ "updated_at": "desc" }]
},
"flags": {
"status": "where.status.$eq",
"keyword": {
"to": "where.customer_name",
"op": "$contain",
"description": "客户名称关键字"
}
}
},
"detail": {
"description": "查看客户详情",
"action": "getOne",
"args": ["id"],
"map": {
"id": {
"target": "id",
"transform": "number"
}
}
}
}
}
}
}上面会生成两个业务入口:
lovrabet crm customer list --status active --keyword 科技
lovrabet crm customer detail 10001它们分别会映射到:
lovrabet data filter --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"where":{"status":{"$eq":"active"},"customer_name":{"$contain":"科技"}},"currentPage":1,"pageSize":20,"orderBy":[{"updated_at":"desc"}]}'
lovrabet data getOne --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"id":10001}'顶层字段
| 字段 | 是否必填 | 说明 |
|---|---|---|
protocol | 否 | 协议名。默认按 lovrabet.service-tree/v1 处理。 |
version | 否 | 配置文件版本。不写时默认 1.0.0。 |
service | 是 | 服务入口。推荐写字符串,例如 "crm";也支持 { "code": "crm", "name": "CRM" }。 |
name / description | 否 | 展示名称和说明,会出现在 help、schema、doctor 中。 |
app | 否 | 单应用快捷绑定。可写 appcode,也可写应用名称。 |
apps | 否 | 多应用绑定表。推荐新配置使用这个字段。 |
appBindings | 否 | 旧命名,语义同 apps,兼容已有配置。 |
defaults | 否 | 服务级默认底层参数,会被资源和动作继承。 |
resources | 推荐 | 业务对象树。新配置优先使用。 |
commands | 兼容 | 旧的扁平命令数组。仍可用,但不再作为推荐写法。 |
service 不是应用编码。不要写成 app-xxxx。一个服务可能串联多个应用,service 应表达业务域,例如 crm、order、store-ops。
app 和 apps:绑定应用
单应用服务可以直接写 app:
{
"service": "crm",
"app": "app-xxxxxxxx"
}如果一个服务会串联多个应用,使用 apps:
{
"service": "customer-success",
"apps": {
"crm": {
"appcode": "app-crmxxxx",
"env": "daily"
},
"order": {
"appcode": "app-orderxxxx",
"env": "daily"
}
}
}资源或动作里通过 appRef 引用应用别名:
{
"resources": {
"customer": {
"appRef": "crm",
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actions": { "list": {} }
},
"order": {
"appRef": "order",
"datasetCode": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"actions": { "list": {} }
}
}
}app、apps、appBindings 只是默认值。用户执行命令时显式指定的应用或环境优先。
resources:定义业务对象树
resources 的每个 key 都会成为命令路径的一段。
{
"resources": {
"customer": {
"name": "客户",
"resources": {
"contact": {
"name": "联系人",
"actions": {
"list": {}
}
}
}
}
}
}这会生成:
lovrabet crm customer contact listResource 常用字段:
| 字段 | 说明 |
|---|---|
name / description | 业务对象展示信息。 |
appRef / app | 引用 apps / appBindings 里的应用别名。多应用服务里常用。 |
datasetCode | 数据集 code,推荐优先使用。 |
datatable / table | 物理表名。未写 datasetCode 时,可按表名解析数据集。 |
defaults / params | 资源级默认底层参数,例如分页、排序、固定条件。 |
actions | 这个业务对象上的动作,例如 list、mine、detail、create。 |
resources | 子业务对象,用于更深层命令。 |
资源上的 appRef、datasetCode、datatable、defaults 会被子资源和动作继承。动作里可以覆盖这些值。
actions:定义业务动作
actions 的每个 key 是命令路径的最后一段。
{
"actions": {
"list": {
"description": "查看客户列表"
},
"detail": {
"description": "查看客户详情",
"action": "getOne",
"args": ["id"],
"map": {
"id": {
"target": "id",
"transform": "number"
}
}
}
}
}Action 常用字段:
| 字段 | 说明 |
|---|---|
description | 命令说明。 |
action | 动作简写。默认是 filter。可写 getOne、create、update、delete,也可写 sql.exec、bff.exec。 |
target | 完整目标写法,可写对象,也可写 data.filter、sql.exec、bff.exec。 |
kind / command | target 的展开写法,例如 { "kind": "data", "command": "filter" }。 |
datasetCode / datatable / table | 覆盖资源继承的数据集定位。 |
sqlCode | SQL 编码。配合 action: "sql.exec" 使用。 |
bffCode / bffId / scriptName | Backend Function 定位。配合 action: "bff.exec" 使用。 |
args | 位置参数。字符串简写会自动变成必填参数。 |
flags | 可选参数。推荐对象写法。 |
defaults / params | 动作级默认底层参数。 |
map / mapTo | 参数映射规则。推荐新配置使用 map,旧配置里的 mapTo 仍可用。 |
risk | 风险等级。只读可省略;写入用 write;删除或不可逆操作用 high-risk-write。 |
action 写法选择
1. 查询数据集列表
不写 action 时,默认就是 data.filter:
{
"resources": {
"customer": {
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actions": {
"list": {
"description": "查看客户列表"
}
}
}
}
}生成:
lovrabet crm customer list映射到:
lovrabet data filter --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{}'2. 查询单条详情
{
"actions": {
"detail": {
"description": "查看客户详情",
"action": "getOne",
"args": ["id"],
"map": {
"id": {
"target": "id",
"transform": "number"
}
}
}
}
}生成:
lovrabet crm customer detail 10001映射到:
lovrabet data getOne --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"id":10001}'3. 执行 SQL
{
"actions": {
"stats": {
"description": "查看客户统计",
"action": "sql.exec",
"sqlCode": "customer_stats",
"flags": {
"startDate": {
"to": "startDate",
"description": "开始日期"
},
"endDate": {
"to": "endDate",
"description": "结束日期"
}
}
}
}
}生成:
lovrabet crm customer stats --start-date 2026-01-01 --end-date 2026-01-31映射到:
lovrabet sql exec --sqlcode customer_stats --params '{"startDate":"2026-01-01","endDate":"2026-01-31"}'4. 执行 Backend Function
复杂写入建议优先封装到 Backend Function,让服务端处理校验、幂等、权限、事务和失败恢复。
{
"actions": {
"create": {
"description": "创建客户",
"action": "bff.exec",
"scriptName": "createCustomer",
"risk": "write",
"flags": {
"customerName": {
"to": "customerName",
"required": true,
"description": "客户名称"
},
"industry": {
"to": "industry",
"description": "所属行业"
}
}
}
}
}生成:
lovrabet crm customer create --customer-name 云兔科技 --industry software映射到:
lovrabet bff exec --name createCustomer --params '{"customerName":"云兔科技","industry":"software"}'args:必须给出的值
args 适合“详情 ID”“编号”这类调用时必须提供的值。
简写:
{
"args": ["id"]
}完整写法:
{
"args": [
{
"name": "id",
"description": "客户编号",
"required": true
}
]
}args 本身只声明“要收什么值”。要把这个值放到底层请求里,还需要写 map:
{
"args": ["id"],
"map": {
"id": {
"target": "id",
"transform": "number"
}
}
}flags:可选查询条件
flags 适合状态、负责人、关键字、时间范围、分页等条件。
推荐对象写法:
{
"flags": {
"status": "where.status.$eq",
"ownerId": {
"to": "where.owner_id",
"op": "$eq",
"type": "number",
"transform": "number",
"description": "负责人 ID"
},
"startDate": {
"to": "where.created_at",
"op": "$gte",
"description": "开始日期"
},
"pageSize": {
"to": "pageSize",
"type": "number",
"transform": "number",
"description": "每页条数"
}
}
}对外使用时,camelCase 会自动转成短横线:
lovrabet crm customer list --owner-id 12 --start-date 2026-01-01 --page-size 50上面会映射到:
{
"where": {
"owner_id": { "$eq": 12 },
"created_at": { "$gte": "2026-01-01" }
},
"pageSize": 50
}Flag 对象字段:
| 字段 | 说明 |
|---|---|
to / target | 映射目标路径。 |
op / operator | 操作符,例如 $eq、$contain、$gte。 |
type | 参数类型:string、number、boolean、json。不写默认 string。 |
transform | 类型转换:string、number、boolean、json。 |
description | 参数说明。 |
required | 是否必填。 |
default | 默认值。 |
enum | 可选值列表。 |
cliName | 对外展示的参数名。不写时由 name 自动转短横线。 |
omitEmpty | 空值时是否跳过映射。 |
map 和 mapTo:把业务参数翻译成底层参数
map / mapTo 回答一个问题:使用者给出的业务参数,要放到底层请求的哪个位置。
推荐新配置使用 map,旧配置中的 mapTo 仍支持。
常见来源
| 来源 | 说明 | 示例 |
|---|---|---|
flags.xxx | 来自可选参数。 | flags.status |
args.xxx | 来自位置参数。 | args.id |
context.xxx / ctx.xxx | 来自当前登录用户或运行上下文。 | context.userId |
const.xxx | 固定值。 | const.deleted |
const | 固定值来源。 | const |
在 map 里,未带前缀的 key 会自动匹配 args 或 flags。比如有 args: ["id"] 时,"id" 会按 args.id 处理。
简写:直接写目标路径
{
"map": {
"status": "where.status.$eq"
}
}等价于把 status 参数放到:
{
"where": {
"status": { "$eq": "<status>" }
}
}完整写法:目标、操作符、转换分开写
{
"map": {
"ownerId": {
"target": "where.owner_id",
"operator": "$eq",
"transform": "number"
}
}
}业务命令:
lovrabet crm customer list --owner-id 12底层参数:
{
"where": {
"owner_id": { "$eq": 12 }
}
}固定条件
固定条件适合表达“默认只看未删除数据”“默认只看某类客户”。
{
"map": {
"const.deleted": {
"target": "where.deleted",
"operator": "$eq",
"value": 0,
"transform": "number"
}
}
}底层参数:
{
"where": {
"deleted": { "$eq": 0 }
}
}当前用户
“我的客户”不要写死某个人的 ID,使用运行上下文:
{
"actions": {
"mine": {
"description": "查看我负责的客户",
"map": {
"context.userId": {
"target": "where.owner_id",
"operator": "$eq",
"transform": "number"
}
}
}
}
}生成:
lovrabet crm customer mine底层参数:
{
"where": {
"owner_id": { "$eq": "<当前登录用户 ID>" }
}
}defaults 和 params:默认底层参数
defaults 和 params 都表示默认底层参数。推荐新配置统一使用 defaults,params 主要用于兼容旧表达。
可以写在三层:
- 顶层
defaults:整个服务都继承。 - resource
defaults:这个业务对象和子对象继承。 - action
defaults:只影响当前动作。
合并顺序是从外到内,越靠近 action 越优先:
service.defaults -> resource.defaults -> action.defaults示例:
{
"defaults": {
"where": {
"deleted": { "$eq": 0 }
}
},
"resources": {
"customer": {
"defaults": {
"pageSize": 20
},
"actions": {
"list": {
"defaults": {
"currentPage": 1,
"orderBy": [{ "updated_at": "desc" }]
}
}
}
}
}
}datasetCode 和 datatable
data 动作需要能定位到数据集。
推荐优先写 datasetCode:
{
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}如果希望按物理表名解析,可以写 datatable 或 table:
{
"datatable": "crm_customer"
}两者至少写一个。datasetCode 最明确,要求是 32 位编码。datatable 适合多环境数据表一致、但数据集编码可能不同的场景。
risk:标记风险等级
| 值 | 适合场景 |
|---|---|
read | 查询、详情、统计。默认值。 |
write | 新建、更新、同步、触发业务动作。 |
high-risk-write | 删除、批量替换、不可逆操作。 |
data 动作会自动推断部分风险:
filter、getOne、aggregate默认是readcreate、batchCreate、update默认是writedelete默认是high-risk-write
Backend Function 和 SQL 默认按 read 处理。只要实际会写数据,就应显式设置 risk: "write" 或 risk: "high-risk-write"。
旧 commands 写法如何升级
旧写法:
{
"service": {
"code": "crm",
"name": "CRM"
},
"appBindings": {
"main": { "appcode": "app-xxxxxxxx" }
},
"commands": [
{
"path": "customer list",
"description": "查看客户列表",
"target": {
"kind": "data",
"command": "filter",
"appRef": "main",
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"flags": [
{
"name": "status",
"type": "string",
"mapTo": "where.status.$eq"
}
]
}
]
}新写法:
{
"service": "crm",
"name": "CRM",
"apps": {
"main": { "appcode": "app-xxxxxxxx" }
},
"resources": {
"customer": {
"appRef": "main",
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actions": {
"list": {
"description": "查看客户列表",
"flags": {
"status": "where.status.$eq"
}
}
}
}
}
}对应关系:
| 旧字段 | 新字段 |
|---|---|
service.code | service |
appBindings | apps |
commands[].path | resources 路径 + actions key |
commands[].target.datasetCode | resource 或 action 的 datasetCode |
commands[].target.command | action 的 action |
commands[].flags[] | action 的 flags 对象 |
commands[].mapTo | action 的 map |
旧 commands 数组仍能导入和执行。新配置优先使用 resources/actions,因为它能自然表达业务对象层级,也避免在 JSON key 或 path 里用空格拼命令。
完整 CRM 示例
{
"service": "crm",
"name": "CRM",
"description": "客户、联系人和跟进记录的业务服务",
"app": "app-xxxxxxxx",
"resources": {
"customer": {
"name": "客户",
"datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"defaults": {
"currentPage": 1,
"pageSize": 20
},
"actions": {
"list": {
"description": "查看客户列表",
"defaults": {
"orderBy": [{ "updated_at": "desc" }]
},
"flags": {
"status": "where.status.$eq",
"keyword": {
"to": "where.customer_name",
"op": "$contain",
"description": "客户名称关键字"
},
"page": {
"to": "currentPage",
"type": "number",
"transform": "number"
},
"pageSize": {
"to": "pageSize",
"type": "number",
"transform": "number"
}
}
},
"mine": {
"description": "查看我负责的客户",
"map": {
"context.userId": {
"target": "where.owner_id",
"operator": "$eq",
"transform": "number"
}
}
},
"detail": {
"description": "查看客户详情",
"action": "getOne",
"args": ["id"],
"map": {
"id": {
"target": "id",
"transform": "number"
}
}
},
"create": {
"description": "创建客户",
"action": "bff.exec",
"scriptName": "createCustomer",
"risk": "write",
"flags": {
"customerName": {
"to": "customerName",
"required": true,
"description": "客户名称"
},
"industry": {
"to": "industry",
"description": "所属行业"
}
}
}
},
"resources": {
"contact": {
"name": "联系人",
"datasetCode": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"actions": {
"list": {
"description": "查看客户联系人",
"flags": {
"customerId": {
"to": "where.customer_id",
"op": "$eq",
"type": "number",
"transform": "number",
"required": true
}
}
}
}
}
}
}
}
}生成的业务入口:
lovrabet crm customer list
lovrabet crm customer mine
lovrabet crm customer detail 10001
lovrabet crm customer create --customer-name 云兔科技 --industry software
lovrabet crm customer contact list --customer-id 10001校验清单
导入前逐项检查:
service是小写短横线格式,例如crm、order、store-ops。- 至少有一个
resources或commands。 - 新配置优先使用
resources/actions。 - 每个 resource key 和 action key 都是小写短横线格式。
- 每个生成的完整命令路径不重复。
- data 动作有
datasetCode或datatable/table。 datasetCode是 32 位编码。- SQL 动作有
sqlCode。 - Backend Function 动作有
bffCode、bffId或scriptName。 flags.type只写string、number、boolean或json。map/mapTo的来源只使用flags.、args.、context.、ctx.、const.,或能自动匹配到已有args/flags的短名。- 写入类动作设置了正确的
risk。 - 多应用服务里,resource 或 action 的
appRef能在apps中找到。
什么时候用旧 commands
只有两种情况继续用 commands:
- 已有配置很多,短期只做小修,不想立刻调整结构。
- 某个服务非常简单,团队确认不需要资源层级。
新建配置优先使用 resources/actions。