让它守规矩:提交条件与一个事务

第 4 课 · 共 7 课 约 8 分钟

提交、批准、拒绝三个 Action:提交条件逐条把关,批准时在一个事务里改两个对象,角色决定谁能做。

本课目标

读完这一课,你将能够

  • 写提交条件,并给每一条配好失败时给人看的那句话
  • 让一个 Action 在一个事务里改两个对象
  • 分清参数限制、提交条件和 roles 各挡什么

提交:两条提交条件

参数限制只看参数自己,比如座位数在 1 到 10000 之间。「申请的座位数要比现在的多」要拿参数和公司当前的座位数比,这是提交条件的事。把下面的文件存成 ontology/04-submit-seat-request.json:

{
  "kind": "actionType",
  "apiName": "submit-seat-request",
  "title": "提交座位申请",
  "description": "客户公司申请把座位数提到新的数目。要比现在多;暂停中的公司不能申请。",
  "schema": {
    "parameters": [
      { "name": "customer", "type": "object", "objectType": "customer", "title": "公司", "required": true },
      { "name": "seats", "type": "integer", "title": "申请的座位数", "required": true, "min": 1, "max": 10000 },
      { "name": "reason", "type": "string", "title": "理由", "maxLength": 200 }
    ],
    "submissionCriteria": [
      {
        "condition": { "type": "comparison", "left": { "param": "seats" }, "operator": "gt", "right": { "param": "customer", "property": "seats" } },
        "failureMessage": "申请的座位数要比现在的多"
      },
      {
        "condition": { "type": "comparison", "left": { "param": "customer", "property": "stage" }, "operator": "isNot", "right": { "literal": "暂停" } },
        "failureMessage": "暂停中的公司不能申请座位"
      }
    ],
    "rules": [
      {
        "type": "createObject",
        "objectType": "seatRequest",
        "values": {
          "requestId": "$uuid",
          "customerId": "$customer",
          "customerName": "$customer.name",
          "currentSeats": "$customer.seats",
          "seats": "$seats",
          "reason": "$reason",
          "status": "submitted",
          "requestedBy": "$user",
          "requestedAt": "$now"
        }
      }
    ],
    "summary": "{customer} 申请座位数 {seats}",
    "actionLog": true,
    "toolDescription": "Submit a seat request for a customer company: the requested seat count must be higher than the current one, and paused companies cannot request."
  }
}

每条提交条件是一个比较:左边和右边可以是参数(param)、对象参数的属性(加 property)、字面量(literal),运算符有 is、isNot、gt、gte、lt、lte 等;不满足时,返回你写在 failureMessage 里的那句话。规则是一条 createObject:$customer 取公司的主键,$customer.name 取公司的名字,$uuid 是新申请的编号。requestedBy 存的是 $user:执行人的账号 ID,不是名字——名字会改、会重名,也不该散落在数据里;界面再把 ID 换成「你」「自动化」这样的说法。$user 的样子随入口不同:开发者 Key 从命令行、SDK、REST 调用时是账号 ID,自动化是 workflow:<应用名>,经 aidc app call 时是 account:<账号 ID>。

aidc semantic define ontology && aidc semantic publish --notes "提交座位申请"

aidc semantic apply submit-seat-request --param customer=acme-east --param seats=20
# 422 申请的座位数要比现在的多
aidc semantic apply submit-seat-request --param customer=acme-pause --param seats=30
# 422 暂停中的公司不能申请座位
aidc semantic apply submit-seat-request --param customer=acme-east --param seats=30 \
  --param reason="新开两个仓库,调度员增加"
aidc semantic objects seatRequest --select customerName,seats,status   # 第一列 __primaryKey 就是申请编号

两条条件都不满足时(暂停的公司、座位数还更少),执行只报第一条;--validate-only 会把每一条的结果都列出来,界面上用它给人一次说全。

批准:一个事务里改两个对象

批准要做两件事:把申请标成已批准,把公司的座位数改成申请的数目。写成两条规则,放进同一个 Action。存成 ontology/05-approve-seat-request.json:

{
  "kind": "actionType",
  "apiName": "approve-seat-request",
  "title": "批准座位申请",
  "description": "批准一张待审批的申请:申请标为已批准,同一个事务里把这家公司的座位数改成申请的数目。只有开发者能批。",
  "schema": {
    "parameters": [
      { "name": "request", "type": "object", "objectType": "seatRequest", "title": "申请", "required": true },
      { "name": "customer", "type": "object", "objectType": "customer", "title": "公司", "required": true },
      { "name": "note", "type": "string", "title": "批复意见", "maxLength": 200 }
    ],
    "submissionCriteria": [
      {
        "condition": { "type": "comparison", "left": { "param": "request", "property": "status" }, "operator": "is", "right": { "literal": "submitted" } },
        "failureMessage": "只能批准待审批的申请"
      },
      {
        "condition": { "type": "comparison", "left": { "param": "request", "property": "customerId" }, "operator": "is", "right": { "param": "customer", "property": "customerId" } },
        "failureMessage": "这张申请不是这家公司的"
      },
      {
        "condition": { "type": "comparison", "left": { "param": "request", "property": "seats" }, "operator": "gt", "right": { "param": "customer", "property": "seats" } },
        "failureMessage": "这张申请已经过时:公司现有的座位数不比它少"
      },
      {
        "condition": { "type": "comparison", "left": { "param": "request", "property": "seats" }, "operator": "gte", "right": { "param": "customer", "property": "members" } },
        "failureMessage": "座位数不能少于成员数"
      }
    ],
    "rules": [
      {
        "type": "modifyObject",
        "objectType": "seatRequest",
        "object": "$request",
        "values": { "status": "approved", "decidedBy": "$user", "decidedAt": "$now", "decisionNote": "$note" }
      },
      {
        "type": "modifyObject",
        "objectType": "customer",
        "object": "$customer",
        "values": { "seats": "$request.seats" }
      }
    ],
    "roles": ["developer"],
    "summary": "批准 {customer} 的座位申请:{seats} 座",
    "actionLog": true,
    "toolDescription": "Approve a pending seat request and set the customer's seat count to the requested number, in one transaction. Developers only."
  }
}

存好后再 define、publish 一次(还是 ontology 目录;没变的定义不受影响):

aidc semantic define ontology && aidc semantic publish --notes "批准座位申请"
01

只能批准待审批的

状态不是 submitted 的申请,批第二遍会被拒。

02

参数之间要对得上

申请和公司是两个参数,条件核对申请的 customerId 就是这家公司。填错了公司,拒绝。

03

别把座位数改小

批准那一刻按公司当前的座位数再核一遍:另一张更大的先批了,这张就过时了。

04

不少于成员数

提交时够用,批准前成员多了,就不够了;条件在批准的那一刻按当时的值再核一遍。

把下面的 2e291b2914258863 换成你自己的申请编号:

aidc semantic apply approve-seat-request \
  --param request=2e291b2914258863 --param customer=acme-east \
  --param note="同意,试点结束前观察一个月" --return-edits --json
"edits": {
  "edits": [
    { "type": "modifyObject", "primaryKey": "2e291b2914258863", "objectType": "seatRequest" },
    { "type": "modifyObject", "primaryKey": "acme-east", "objectType": "customer" }
  ],
  "addedObjectCount": 0,
  "modifiedObjectsCount": 2,
  …
}

两条规则按顺序执行,在同一个事务里落库:任何一条写不进去,两个对象都保持原样。modifiedObjectsCount 是 2,公司的座位数从 20 变成了 30,aidc semantic objects customer 里能看到。再批一次同一张申请,返回「只能批准待审批的申请」。

拒绝,和谁能做

拒绝只改申请,不动公司,而且必须写批复意见。存成 ontology/06-reject-seat-request.json:

{
  "kind": "actionType",
  "apiName": "reject-seat-request",
  "title": "拒绝座位申请",
  "description": "拒绝一张待审批的申请,必须写批复意见。不改公司的座位数。",
  "schema": {
    "parameters": [
      { "name": "request", "type": "object", "objectType": "seatRequest", "title": "申请", "required": true },
      { "name": "note", "type": "string", "title": "批复意见", "required": true, "maxLength": 200 }
    ],
    "submissionCriteria": [
      {
        "condition": { "type": "comparison", "left": { "param": "request", "property": "status" }, "operator": "is", "right": { "literal": "submitted" } },
        "failureMessage": "只能拒绝待审批的申请"
      }
    ],
    "rules": [
      {
        "type": "modifyObject",
        "objectType": "seatRequest",
        "object": "$request",
        "values": { "status": "rejected", "decidedBy": "$user", "decidedAt": "$now", "decisionNote": "$note" }
      }
    ],
    "roles": ["developer"],
    "summary": "拒绝 {customerName} 的座位申请",
    "actionLog": true,
    "toolDescription": "Reject a pending seat request with a note. Does not change the customer's seat count. Developers only."
  }
}
aidc semantic define ontology && aidc semantic publish --notes "拒绝座位申请"

note 是必填参数,不填,校验会指到它(「批复意见 必填」)。批准和拒绝都写了 "roles": ["developer"]:只有开发者能执行。提交没写 roles,缺省是 developer、member、editor 都行。

提交条件roles
看什么参数,以及对象参数当前的属性值谁在执行:developer、member、editor…
写在哪submissionCriteria,每条配一句失败信息roles 数组
挡下时你看到422,你事先写好的那句话403,「只有 developer 能执行「批准座位申请」。」
例子申请的座位数要比现在的多批准只给开发者

只读访问(分享链接、只读分享)永远不能执行 Action,连「只校验」也不行,会得到 403。这一条不用你写。

三个坑

  • 遇到空值,比较不成立。gte、gt、lt、lte 碰到没有值的属性一律判不通过。所以登记客户时 members 是必填的;关键属性要在新建时就写上。
  • 提交条件不做算术。比不了「差值不超过 5」这种。把要比的数先算好存进属性(比如存一份快照),或者直接写成字面量(第 7 课的 50 座)。
  • 执行只报第一条。想让人一次看到所有问题,界面就先 --validate-only,逐条列出来。

要点

  • 参数限制只看参数自己;要拿参数和对象当前的值比,写成提交条件,并配一句人看得懂的失败信息。
  • 一个 Action 可以有多条规则,在同一个事务里落库:要么两个对象都改,要么都不改。
  • 条件在执行的那一刻按当前的值再核一遍,所以过时的申请、后来多出的成员,批准时都拦得住。
  • roles 管谁能做,返回 403;提交条件管做得对不对,返回 422。只读访问永远不能执行。

练一练

把每一种失败跑一遍

在你自己的公司里做;每种失败都先 --validate-only,再执行。

座位数不比现在多、暂停的公司、公司不存在、缺 seats、合格:各得到什么信息?哪一种是参数不通过,哪一种是提交条件不通过?

小测

选一个答案,马上看解析。

Q1「申请的座位数要比公司现在的多」应该写成什么?

Q2批准时,公司的那条规则写入失败了(比如公司刚被删除),申请会变成已批准吗?

Q3一个 member 账号通过应用执行「批准座位申请」(roles 只有 developer),会得到什么?

延伸阅读