BQP - 业务查询协议

业务查询协议,简称BQP(Business Query Protocol),它是一种远程过程调用(RPC)协议。 本文档定义业务接口如何调用及返回,如何规范描述接口,以及定义通用对象操作接口。

请求由接口名(action),参数(param),数据(data)三部分构成,表示为action(param)(data),其中参数或数据可以缺省,如action(param)action()(data)。 参数一般是键值对,而数据的内容和形式则由具体接口定义。

接口返回形式为[code, retData, ...]的JSON数组,至少为两个元素。当调用成功时,code为0,返回数据retData由接口原型定义。 调用失败时(也称为异常),code为非0错误码,retData为错误信息。 返回数组中其它内容一般为调试信息。

假如接口原型如下:

fn(p1, p2?)(data) -> {field1, field2}

其中fn为接口名,p1, p2是两个参数,且p2可以缺省。第二个括号表示需要传输数据(数据格式会特别说明)。 箭头后面部分是调用成功时的返回值,如果没有箭头后面部分,则表示不关心返回值,默认返回字符串“OK”。 调用成功后返回JSON数组示例: [0, {"field1": "value1", "field2": "value2"}]

本文档调用示例使用JS函数callSvr表示:

callSvr(调用名, URL参数/可选, $.noop(表示空的回调函数,用来分隔URL和POST参数), POST参数/可选, 其它选项/可选)

比如,下面表示同时传了URL参数id和POST参数amount:

callSvr("Ordr.set", {id: 100}, $.noop, {amount: 99.9});

1 接口通讯协议

本章定义业务查询协议的实现方式,如何表示请求(调用名、参数、数据)和返回。

业务查询协议基于HTTP协议实现,以下列接口为例:

fn(p1, p2) -> {field1, field2}

以下假定接口服务的URL基地址(BASE_URL)为/api。 该接口可以使用HTTP GET请求实现:

GET /api/fn?p1=value1&p2=value2

或表示为

callSvr("fn", {p1: "value1", p2: "value2"})

也可以使用HTTP POST请求实现:

POST /api/fn
Content-Type: application/x-www-form-urlencoded;charset=utf-8

p1=value1&p2=value2

或表示为

callSvr("fn", $.noop, {p1: "value1", p2: "value2"})

POST内容也可以使用json格式,如:

POST /api/fn
Content-Type: application/json;charset=utf-8

{"p1":"value1","p2":"value2"}

参数允许部分出现在URL中,部分出现在POST内容中,如

POST /api/fn?p1=value1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

p2=value2

或表示为

callSvr("fn", {p1: "value1"}, $.noop, {p2: "value2"})

如果URL与POST内容中出现同名参数,最终以URL参数为准。

接口名为URL基地址后一个词(常称为PATH_INFO),如URL/api/fn中接口名为“fn”。 如果难以实现,也可以使用URL参数ac表示接口名,即URL中/api?ac=fn&p1=value1&p2=value2中接口名也是“fn”。

【必须使用HTTP POST的情形】

如果接口定义中有请求数据(即在接口原型中用两个括号),如:

fn(p1,p2)(p3,p4) -> {field1, field2}

这时必须使用HTTP POST请求,参数只能通过URL传递,数据通过POST内容传递:

POST /api/fn?p1=value1&p2=value2
Content-Type: application/x-www-form-urlencoded;charset=utf-8

p3=value3&p4=value4

注意数据的格式应通过HTTP头Content-Type正确设置,一般应支持“application/x-www-form-urlencoded”或“application/json”格式。 少数例外情况应特别指出,比如上传文件接口upload一般设计为使用HTTP头“Content-type: multipart/form-data”,应在接口文档中明确说明。

协议规定:

服务端在返回JSON格式数据时应如下设置HTTP头属性:

Content-Type: text/plain; charset=UTF-8

注意:不采用“application/json”类型是考虑客户端可以更自由的处理返回结果。

服务端应避免客户端对返回结果缓冲,一般应在HTTP响应中加上

Cache-Control: no-cache

以下面的接口描述为例:

获取订单:
getOrder(id) -> {id, dscr, total}

一次成功调用可描述为:

getOrder(id=101) -> {id: 101, dscr: "套餐1", total: 38.0}

它表示:发起HTTP请求为 GET /api/getOrder?id=101(当然也可以用POST请求),服务端处理成功时返回类型为{id, dscr, total}

HTTP/1.1 200 OK

[0, {"id": 101, "dscr": "套餐1", "total": 38.0}]

关于返回类型表述方式详见后面章节描述。

服务端处理失败时返回示例:

HTTP/1.1 200 OK

[1, "未认证"]

错误码及错误信息在应用中应明确定义,协议规定以下错误码:

enum {
    E_ABORT=-100; // "取消操作"。要求客户端不报错,不处理。
    E_AUTHFAIL=-1; // "认证失败"
    E_OK=0;
    E_PARAM=1; // "参数不正确"
    E_NOAUTH=2; // "未认证", 一般要求客户端引导用户到登录页,或尝试自动登录
    E_DB=3; // "数据库错误"
    E_SERVER=4; // "服务器错误"
    E_FORBIDDEN=5; // "禁止操作",用户没有权限调用接口或操作数据
}

1.1 关于空值

假如传递参数a=1&b=&c=hello,或JSON格式的{a:1, b:null, c:"hello"},其中参数“b”值为空串。 一般情况下,参数“b”没有意义,即与a=1&c=hello意义相同。

在某些场合,如通用对象保存接口{Obj}.set,在POST内容中如果出现“b=”, 则表示将该字段置null。在这些场合下将单独说明。

1.2 多应用支持与应用标识

接口应支持多个应用同时访问,例如按登录角色划分,常见有用户端应用、员工端应用等。

每个客户端应用要求有唯一应用标识(如果没有,缺省为“user”,表示用户端应用),以URL参数"_app"指定。 在每次接口请求时,客户端框架应自动添加该参数。

应用标识(称为app或appName)对应一个应用类型(称为appType),如应用标识“user”, “user2”, “user-keyacct”对应同一应用类型“user”,即应用标识的第一个词(不含结尾数字)作为应用类型。

使用同一接口服务的不同应用类型的应用,如果在浏览器的两个Tab页中分别打开,两者不应相互影响,如用户端的退出登录不会导致员工端的应用也退出登录。 而同一应用类型和不同应用如果在浏览器中同时打开,其会话状态可以共享,比如当一个应用登录后,另一个应用也处于登录状态。

习惯上常用以下应用类型:

一般建议使用标准的HTTP Cookie来实现会话,且以应用类型决定HTTP会话中的Cookie项的名字:

用于HTTP会话的Cookie名={应用类型}id

例如,应用标识为“emp”(表示员工端), 当第一次接口请求时:

GET /api/fn?_app=emp

服务端应通过HTTP头指定会话标识,如:

SetCookie: empid=xxxxxx

1.3 测试模式及调试等级

接口服务可配置为“测试模式”(TEST_MODE),这种模式用于开发和自动化测试,建议的功能有:

接口服务可配置调试等级为0到9,向前端输出不同级别的调试信息。一般设置为9(最高)时,可以查看SQL调用日志,便于调试SQL语句。 调试信息仅在测试模式下生效。

线上生产环境不可设置为测试模式。 当前端发现服务处于测试模式,应给予明确提示。

1.4 通用接口格式参数

接口默认返回格式为[0, 数据, ...],支持以下通用URL参数:

示例:callSvr("Ordr.get", {res: "id, tm, amount"})假设返回[0, {id: 100, tm: '2020-10-10', amount: 380.5}],那么:

2 接口描述

接口描述应包括接口原型和应用逻辑的说明。

接口原型包括接口名、参数、请求数据、返回值的声明。应用逻辑常包括接口权限、字段自动完成逻辑、字段检查逻辑、关联数据添加或更新逻辑等。

示例:

获取订单
Ordr.get(id) -> {id, status, storePos, @orderLog}

参数:

- id: Integer.

返回:

- id: Integer.
- status: enum(CR-创建,PA-已付款,CA-已取消,RE-已完成)。订单状态。
- storePos: Coord="经度, 纬度". 商户坐标.
- orderLog: [{id, tm, ac, dscr}]. 订单日志。

- ac: enum(CR-创建,PA-已付款,CA-已取消,RE-已完成). 操作类型.

应用逻辑:

- 权限:AUTH_USER

上例参数或返回中的id, status等字段如果含义及类型明确,或是在对象对应的数据模型设计文档中已提及,这里也可省略不做介绍。 storePos是一个序列化类型(以字符串表示的复杂类型),称为Coord类型,特别标明。 而orderLog是一个复杂结构,应分解介绍其内部属性,其中id, tm等属性因含义明确省略了介绍。

2.1 接口原型描述

接口名使用驼峰式命名规则,一般有两种形式,1)函数调用型,以小写字母开头,如getOrder;2)对象调用型,对象名首字母为大写,后跟调用名,中间以“.”分隔,如Order.get

在接口原型中,以“?”结尾的参数字段、数据字段或返回字段表示该字段可能缺省,如

fn(p1, p2?, p3?=1) -> {attr1, attr2?}

其中,参数p3的缺省值是1,p2缺省值是0或空串""或null(取决于基本类型是数值型,字符串还是对象等)。 返回对象中,attr1是必出现的属性,而attr2可能没有(接口说明中应描述何时没有)。

接口原型中应描述参数或返回的类型。类型可能是数值、字符串这些基本类型,也可能是对象、数组、字典及其相互组合而成的复杂类型,或虽然是一个字符串但表示某个复杂类型的序列化。

基本类型不可再细分,其类型一般通过名称暗示,如:

对于复杂类型,其描述方法用类似JSON格式来解析其中对象、数组、字典这些结构的组合,举例列举如下:

{id, name}

一个简单对象,有两个字段id和name。例:{id: 100, name: "name1"}

[id…][id]

一个简单数组,每个元素表示id。例:[100, 200, 400], 每项为一个id

[id, name]

一个简单数组,例:[100, "liang"],第一项为id, 第二项为name

[ [id, name] ]varr(id, name)

简单二维数组,又称varr(value array), 如 [ [100, "liang"], [101, "wang"] ].

[{id, name}]objarr(id, name)

一个数组,每项为一个对象,又称objarr。例:[{id: 100, name: "name1"}, {id: 101, name: "name2"}]

tbl(id, name)

压缩表对象,常用于返回分页列表。其详细格式为 {h: [header1, header2, ...], d:[row1, row2, ...], nextkey?, total?},例如

{
  h: ["id", "name"],
  d: [[100, "myname1"], [200, "myname2"]]
}

压缩对象支持分页机制(paging),返回字段中可能包含“nextkey”,“total”等字段。 详情请参考后面章节“分页机制”.

在类型描述时,可以用“@”符号表示一个数组属性,而对象或字典一般用“%”表示,如:

获取订单接口:
Ordr.get(id) -> { id, dscr, %addr, @items }

返回

- addr: {country, city}. 收货地址
- items: [{id, name, qty}]. 订单中的物品。

注意:

以上对类型的描述,使用的是一种层层剖析的形式化表达方法,请参考蚕茧表示法

除了基本类型和复杂类型,有时传递参数还会使用一个字符串来代表复杂结构,称为序列化类型。 常用的有:

2.2 应用逻辑描述

在接口描述的应用逻辑说明中应包括接口权限说明。

权限在设计接口时定义,常用的定义示例如下:

权限一般名为PERM_XXX,特别地,登录类型是一种特殊的权限,一般定义名称为AUTH_XXX

如果接口未明确指定权限,则认为是AUTH_GUEST.

3 通用对象操作接口

业务接口包括函数调用型接口和对象调用型接口。

函数型接口名称一般为动词或动词开头,如queryOrder, getOrder等。对象型接口的格式为{对象名}.{动作}, 如 “Order.get”, “Order.query”等。

接口服务框架应支持对象型接口的以下标准操作:add, set, query, get, del。 这些操作提供对象的基本增删改查(CRUD)以及列表查询、统计分析、导出等服务,称为通用对象接口。

在做接口设计时,应以通用对象接口为基础,按业务逻辑需要进行定制形成专用接口,如进行权限限制、指定允许的操作类型(如只能get/set,不能add/del)、只读字段、隐藏字段等。

下面将分别定义这些操作,其中用Obj代指对象实际名称。

3.1 基本增删改查操作

【添加操作】

接口原型:

Obj.add(uniKey?, uniKeyMode?=set)(POST fields...) -> id
Obj.add(res)(POST fields...) -> {fields...} (返回的字段由res参数指定)

对象的属性通过POST请求内容给出,为一个个键值对。 添加完成后,默认返回新对象的id, 如果想多返回其它字段,可设置res参数,如

callSvr("Ordr.add", $.noop, {status:"CR", total:100})
返回
809

callSvr("Ordr.add", {res:"id,status,total"}, $.noop, {status:"CR", total:100})
返回
{id: 810, status:"CR", total: 100}

对象id支持自动生成。

添加时支持检查重复数据,由uniKey和uniKeyMode参数来控制:

这两个参数也常用于批量导入或批量更新,详见批量导入数据章节,即batchAdd接口。

只做数据验证,不添加:

【复制操作】

复制指定id的对象,生成新的对象:

Obj.dup(id) -> [id1, ...]

返回新对象的id数组,与原对象一一对应。

【更新操作】

接口原型:

Obj.set(id)(POST fields...)

与add操作类似,对象属性的修改通过POST请求传递,而在URL参数中需要有id标识哪个对象。

示例:

callSvr("Obj.set", {id: 809}, $.noop, {status:"PA", empId:10})
返回
"OK"

如果未指定返回值,一般默认返回“OK”。下面示例也将省略返回值。

如果要将某字段置空, 可以用空串或“null” (小写)。例如:

callSvr("Obj.set", {id: 809}, $.noop, {picId:"", empId:"null"})
(实际传递参数的形式为 "picId=&empId=null",注意是字符串"null",不是直接的null)

这两种方式都是将字段置空。 注意:一般情况下,接口传参数“picId=”这样的,参数会被忽略,相当于没有设置该字段。

另外注意,上例是设置字段为null,而不是设置成空串"“。 如果要将字符串置空串(一般不建议使用),可以用”empty", 例如:

callSvr("Obj.set", {id: 809}, $.noop, {sn: "empty"})

假如sn是数值类型,会导致其值为0或0.0。

支持根据条件批量更新,使用setIf或batchSet接口:

Obj.setIf(cond)(POST fields...)
Obj.batchSet(cond)(POST fields...)

注意:setIf接口相当于执行1条数据库SQL UPDATE语句,不执行业务逻辑,速度快; 而batchSet接口先根据条件查出所有记录,然后对每个记录逐一调用set接口进行更新,会执行每条记录更新时的业务逻辑。

示例:

callSvr("Obj.setIf", {cond: "tm>='2010-1-1' and tm<'2011-1-1'"}, $.noop, {dscr: "已处理"});

【获取对象操作】

接口原型:

Obj.get(id, res?) -> {fields...}

默认返回所有暴露的属性,通过res参数可以指定需要返回的字段。

【删除操作】

接口原型:

Obj.del(id)

根据id删除一个对象,例如:

callSvr("Obj.del", {id: 809})

支持根据条件进行批量删除,使用delIf接口:

Obj.delIf(cond)
Obj.batchDel(cond)

delIf与batchDel的区别,类似于setIf与batchSet的区别。前者只根据条件执行一条SQL DELETE语句,速度快;后者是先查出符合条件的所有记录,再逐一记录删除(走del接口)。

示例:

callSvr("Obj.delIf", {cond: "tm>='2010-1-1' and tm<'2011-1-1'"});

3.2 查询操作

接口原型:

查询列表(默认压缩表格式):
Obj.query(res?, cond?, distinct?=0, pagesz?=20, pagekey/page?) -> tbl(fields...) = {nextkey?, total?, @h, @d}

查询列表 - 对象列表格式:
Obj.query(fmt=list/one/one?/array/hash/multihash, ...) -> {nextkey?, total?, @list=[obj1, obj2...]}

分组统计:
Obj.query(gres, ...) -> tbl(fields...)

导出查询列表到文件:
Obj.query(fmt=csv/txt/excel, ...) -> 文件内容

查询接口非常灵活,不仅支持条件组合查询、排序、指定输出字段等,还支持分页列表、分组统计、导出文件等。

查询操作的参数可参照SQL语句来理解:

res
String. 指定返回字段, 多个字段以逗号分隔,例如, res=“field1,field2”。字段前不可加表名或别名(alias),如“t0.id”或“id as userId”不合法。 在res中允许使用部分统计函数如sumcount, 这时必须指定字段别名, 如count(id) cnt, sum(qty*price) total, count(distinct addr) addrCnt.
cond
String. 指定查询条件,语法可参照SQL语句的“WHERE”子句。例如:cond="field1>100 AND field2='hello'", 注意使用UTF8+URL编码, 字符串值应加上单引号.
orderby
String. 指定排序条件,语法可参照SQL语句的“ORDER BY”子句,例如:id desc,也可以多个排序如:tm desc,status (按时间倒排,再按状态正排)
distinct
Boolean. 如果为1, 生成SELECT DISTINCT ...查询.

尽管类似SQL语句,但对参数值有一些安全限制:

3.2.1 查询条件(cond)

用参数cond指定查询条件, 如:

{cond: "type='A' and name like '%hello%'"}

也可以使用键值对方式:

{cond: {type: "A", name: "~hello"} }

以下情况都不允许:

left(type, 1)='A'  -- 条件左边只能是字段,不允许计算或函数
type=type2  -- 字段与字段比较不允许
type in (select type from table2) -- 子查询不允许

cond参数可以同时在URL参数和POST参数中指定,支持字符串、数组、键值对方式指定查询条件。

3.2.2 查询结果格式(fmt)

查询结果可以以指定形式返回, 缺省返回压缩表类型即“h/d”格式,例如:

{
    h: ["id", "name"],
    d: [[1, "jerry"], [2, "tom"]]
    nextkey: ... (用于分页,注意默认分页20条)
}

由于不会每行重复传输字段名,压缩表类型一般传输效率更高。

【list与array格式】

如果指定{fmt: "list"},则返回对象列表格式:

{
    "list": [
        {"id": 1, "name": "jerry"},
        {"id": 2, "name": "tom"}
    ],
    nextkey: ... (用于分页,注意默认分页20条)
}

如果指定{fmt: "array"},则返回数组对象列表格式(相当于list格式的list内容部分),注意此时不支持分页,返回后端限制的最大行数的数据(默认1000,最大可调到10000),常用于已知行数有限的查询:

[
    {"id": 1, "name": "jerry"},
    {"id": 2, "name": "tom"}
]

【one与one?格式】

如果指定{fmt: "one"},则只以对象格式返回一行,类似get接口:

{"id": 1, "name": "jerry"}

且如果查询不到数据,会抛出错误(也是与get接口类似)。

如果查询不到数据时不想抛出错误,而是返回null,可以用{fmt: "one?"}参数。

特别地,如果返回数据只有一列,one?格式则直接返回该列值。 示例:查询订单数,返回只有一列cnt。

callSvr("Ordr.query", {res: "COUNT(*) cnt", fmt: "one?"})

假如有99个订单,使用fmt:"one?"直接返回99,如果使用fmt:"one"则返回{cnt: 99}

【hash与multihash格式】

如果指定{fmt: "hash"},则以映射表格式返回:

{
    1: {"id": 1, "name": "jerry"},
    2: {"id": 2, "name": "tom"}
}

它等价于{fmt: "hash:id"},即hash后未指定字段时,默认取第一个字段做为hash key。

如果指定{fmt: "hash:name"}:

{
    "jerry": {"id": 1, "name": "jerry"},
    "tom": {"id": 2, "name": "tom"}
}

如果指定{fmt: "hash:id,name"}:

{1: "jerry", 2: "tom"}

如果指定{fmt: "hash:name,id"}:

{"jerry": 1, "tom": 2}

multihash与hash类似,只是用数组表示结果,所以就算出现key重名时也不会覆盖,示例:指定{fmt: "multihash"}

{
    1: [ {"id": 1, "name": "jerry"} ],
    2: [ {"id": 2, "name": "tom"} ]
}

如果指定{fmt: "multihash:name,id"}:

{"jerry": [ 1 ], "tom": [ 2 ]}

【tree树型结构】

例如如下{id,fatherId}线性结构数组中,数组的每个元素中有个fatherId字段指向父结点的id属性:

[
    {"id":1},
    {"id":2, "fatherId":1},
    {"id":3, "fatherId":2},
    {"id":4, "fatherId":1}
]

如果指定{fmt: "tree"},返回转为树型结构{id,children}:

[
    {"id":1, "children": [
        {"id":2, "fatherId":1, "children": [
            {"id":3, "fatherId":2},
        ]},
        {"id":4, "fatherId":1}
    ]},
]

可以通过URL参数treeFields重定义各字段名,默认值为id,fatherId,children,设置示例:{treeFields:'code,fatherCode'}{treeFields:'code,fatherCode,subtree'}

3.2.3 查询结果支持分页

参数pagesz/pagekey等与返回分页列表有关,详细介绍请参考“查询分页机制”章节。

3.2.4 导出文件

查询结果支持导出到文件

在对象查询接口中添加参数“fmt”,可以输出指定格式,一般用于列表导出。参数:

fmt
Enum(csv,txt,excel,excelcsv,html). 导出Query的内容为指定格式。格式说明如下:

在实现时,注意设置正确的HTTP头,如csv文件:

Content-Type: application/csv; charset=UTF-8
Content-Disposition: attachment;filename=1.csv

导出txt文件设置HTTP头的例子:

Content-Type: text/plain; charset=UTF-8
Content-Disposition: attachment;filename=1.txt

示例:导出以逗号分隔的表格文本

var url = makeUrl("Store.query", {
    res: "id,name,addr",
    fmt: "csv",
    pagesz: -1
})
window.open(url); // 下载文件

注意,由于默认会有分页,要想导出所有数据,一般可指定分页大小为-1(后端最大限制一般为10000条,可在后端调整)

3.2.5 枚举名字映射

例如有状态字段,定义为:

在query接口中,可以通过res指定映射关系,示例:

callSvr("Ordr.query", {res: "id, status =CR:新创建;CA:已取消"})

如果该字段值为CR,应返回“新创建”,返回示例:

[ {id: 1, status: "新创建"} , {id: 2, status: "已取消"} ]

也可定义空值(null)或空串("")的显示,如: status =CR:新创建;CA:已取消;:(null),表示将空值显示为(null)

如果字段值是多个逗号分隔的值列表,如“CR,CA”,则应返回“新创建,已取消”。

可以与别名一起使用,示例:

callSvr("Ordr.query", {res: "id 编号, status 状态=CR:新创建;CA:已取消"})

返回示例:

[ {编号: 1, 状态: "新创建"} , {编号: 2, 状态: "已取消"} ]

该特性在使用query接口导出文件时特别有用。

3.2.6 汇总统计

通过在res中指定SUM等字段,可以返回汇总信息,如:

callSvr("Ordr.query", {
    res: "COUNT(id) cnt, SUM(amount) amount",
    fmt: "one" // 由于结果必然只有一行,指定one格式更方便
})

返回示例:

{cnt: 82, amount: 35340}

考虑安全性与便利性,res中只能使用白名单中的安全统计函数,包括:MAX, MIN, AVG, SUM, COUNT, SUMIF, COUNTIF. 函数名不区分大小写。

其中SUMIF和COUNTIF是扩展函数,便于进行带条件的统计,如统计今日(假设日期为2020-10-10)、本月、今年的累计订单数和订单金额:

callSvr("Ordr.query", {
    res: "COUNTIF(createTm>='2020-10-10') 今日订单数, COUNTIF(createTm>='2020-10-1') 本月订单数, COUNTIF(createTm>='2020-1-1') 今年订单数, " +
        "SUMIF(createTm>='2020-10-10', amount) 今日订单额, SUMIF(createTm>='2020-10-1', amount) 本月订单额, SUMIF(createTm>='2020-1-1', amount) 今年订单额",
    fmt: "one" 
})

它等价于多次下面带cond参数的调用:

callSvr("Ordr.query", {
    res: "COUNT(1) 今日订单数, SUM(amount) 今日订单额",
    cond: "createTm>'2020-10-10'",
    fmt: "one" 
})

COUNTIF也支持指定统计字段,如COUNTIF(createTm>='2020-10-10', userPhone)COUNTIF(createTm>='2020-10-10', DISTINCT userPhone), 前者表示统计指定条件下且userPhone非空的个数,后者表示统计指定条件下且userPhone非空且不重复的个数,其底层实现类似于COUNT(DISTINCT IF(createTm>='2020-10-10', userPhone, NULL))

也可以在普通查询的同时指定要统计哪些列,使用statRes字段。

statRes
指定统计字段,会在分页返回结果中添加stat对象。注意fmt参数不能是one, array, hash等,只允许是list或默认(即hd格式)。 statRes的写法与res相同,支持COUNT/COUNTIF等白名单内的统计函数。

请求示例:

callSvr("Ordr.query", {
    res: "id, createTm, amount",
    statRes: "COUNT(id) cnt, SUM(amount) amount",
})

返回示例:

{
    h: ["id", "createTm", "amount"],
    d: [
        [ 100, "2015-1-1 10:10:10", 1000],
        [ 101, "2015-1-2 10:11:10", 1200],
        ...
    ],
    stat: {cnt: 82, amount: 35340},
    nextkey: 9
}

如果上面指定fmt: "list"则返回示例:

{
    list: [
        { id: 100, createTm: "2015-1-1 10:10:10", amount: 1000 },
        { id: 101, createTm: "2015-1-2 10:11:10", amount: 1200 },
        ...
    ],
    stat: {cnt: 82, amount: 35340},
    nextkey: 9
}

3.2.7 分组统计

主要通过gres参数实现查询结果分组:

gres
String. 分组字段。如果设置了gres字段,则res参数中每项应该带统计函数,如“sum(cnt) sum, count(id) userCnt”. 最终返回列为gres参数指定的列加上res参数指定的列; 如果res参数未指定,则只返回gres参数列。 如果指定参数gresHidden=1,gres字段则不会自动加到最终结果列中。

例:统计2015-2016两年间,按年份、状态分类(如已付款、已评价、已取消等)的各类订单的总数和总金额。

callSvr("Ordr.query", {
    gres: "y,status", res: "count('A') totalCnt, sum(amount) totalAmount",
    cond: "tm>='2015-1-1' and tm<'2017-1-1'"
})

返回内容示例:

{
    h: ["y", "status", "totalCnt", "totalAmount"],
    d: [
        [ 2015, "PA", 1130, 14420 ],  // 已付款,共1130单,14420元
        [ 2015, "CA", 2, 38 ], // 取消的订单
        [ 2016, "PA", 170, 3390 ],
        [ 2016, "CA", 9, 220 ],
        [ 2016, "RA", 1530, 15580 ], // 已评价的订单
    ]
}

以下为约定时间统计字段:

后端可以默认提供这些时间统计字段,也可以由前端指定一个时间字段,生成这些统计字段:

tmField
String. 指定时间字段,基于该字段生成时间统计字段(y,m,d等虚拟字段)。

示例:按付款时间payTm来统计每年订单数:

callSvr("Ordr.query", {
    res: "count(*) cnt",
    gres: "y",
    tmField: "payTm"
});

【行列转置】

在做数据透视表展示统计结果时,常常用到行列转置,可用以下参数:

pivot
String. 设置行列转置。
pivotCnt
Integer. 可选,默认统计列为最后1列,若最后两列都是是统计列,可以设置为2.

例:上面示例中,将状态status列转置到行上:

callSvr("Ordr.query", {
    gres: "y,status", res: "count('A') totalCnt, sum(amount) totalAmount",
    cond: "tm>='2015-1-1' and tm<'2017-1-1'",
    pivot: "status",
    pivotCnt: 2
})

返回内容示例:

{
    h: ["y", "PA","CA","RA"],
    d: [
        [ 2015, [1130, 14420], [2, 38], [0, 0] ],
        [ 2016, [170, 3390], [9, 220], [1530, 15580] ]
    ]
}

转置到列上的数据如果为null,则以“(null)”来显示;如果是空串,不做特殊处理。

pivot参数可以设置多列,以逗号分隔,如将年、月显示到列上:

callSvr("Ordr.query", {
    gres: "y,m,status", res: "count('A') totalCnt",
    cond: "tm>='2015-1-1' and tm<'2017-1-1'",
    pivot: "y,m"
})

转置到列后的字段以“-”拼接,如果有值为null的,以“(null)”显示。 返回内容示例:

{
    h: ["status", "2020-1", "2020-2", "2020-(null)", "(null)-(null)"],
    d: [
        [ "PA", 120, 230, 1, 0],
        [ "CA", 300, 310, 0, 2]
    ]
}

【行列汇总】

没有pivot时,用sumFields参数指定要统计的列,支持多列;有pivot时,用pivotSumField参数指定新添加的统计列的名字,只有一列,不存在多列。

sumFields
String. 在数据最后添加汇总行,对一个或多个字段进行汇总。当有pivot参数时,该选项无效,应使用pivotSumField参数(下面有介绍)。 注意如果数据仅有一行,不添加汇总行。注意如果有分页,即数据在一页内显示不完,只会累加当前页的数据。

示例:简单查询,汇总其中一列

callSvr("Ordr.query", {
    res: "id, createTm, amount",
    sumFields: "amount"
})

返回内容示例:

{
    h: ["id", "createTm", "amount"],
    d: [
        [ 100, "2015-1-1 10:10:10", 1000],
        [ 101, "2015-1-2 10:11:10", 1200],
        [ "合计", null, 2200 ], // 自动添加的汇总行,对指定列进行汇总
    ]
}

如果一页无法返回所有数据,又想在汇总中累计所有数据,要么调整pagesz参数让数据尽可能在一页内返回,要么额外使用statRes参数(前面有介绍)得到统计字段,如:

callSvr("Ordr.query", {
    res: "id, createTm, amount",
    statRes: "SUM(amount) amount",
    sumFields: "amount"
})

返回内容示例:

{
    h: ["id", "createTm", "amount"],
    d: [
        [ 100, "2015-1-1 10:10:10", 1000],
        [ 101, "2015-1-2 10:11:10", 1200],
        ...
        [ "合计", null, 50200 ], // 自动添加的汇总行,对指定列进行汇总
    ],
    // statRes参数将会自动添加stat对象,表示各统计列
    stat: { amount: 50200 },
    nextkey: 9
}

注意:sumFields中的字段,是优先从stat对象中获取的,如果在stat中不存在,才做页内累加。

示例:分组统计查询,汇总两列

callSvr("Ordr.query", {
    gres: "y,status", res: "count('A') totalCnt, sum(amount) totalAmount",
    sumFields: "totalCnt, totalAmount"
})

返回内容示例:

{
    h: ["y", "status", "totalCnt", "totalAmount"],
    d: [
        [ 2015, "PA", 1130, 14420 ],  // 已付款,共1130单,14420元
        [ 2015, "CA", 2, 38 ], // 取消的订单
        [ 2016, "PA", 170, 3390 ],
        [ 2016, "CA", 9, 220 ],
        [ 2016, "RA", 1530, 15580 ], // 已评价的订单
        [ "合计", null, 2841, 33648 ], // 自动添加的汇总行,对指定的两列进行汇总
    ]
}
pivotSumField
String. 例如设置值为“合计”,则会在每行添加一个名为“合计”的汇总列,且在数据最后添加一个汇总行。 注意:如果行转置后只有一行,则不显示统计列;如果总共只有一行,则不显示统计行。

示例:

callSvr("Ordr.query", {
    gres: "y,status", res: "count('A') totalCnt, sum(amount) totalAmount",
    pivot: "status",
    pivotSumField: "合计"
})

返回内容示例:

{
    h: ["y", "PA","CA","RA", "合计"], // 自动添加汇总列"合计"
    d: [
        [ 2015, [1130, 14420], [2, 38], [0, 0], [1132, 14458] ],
        [ 2016, [170, 3390], [9, 220], [1530, 15580], [1709, 19190] ],
        [ "合计", [1300, 17810], [11, 258], [1530, 15580], [2841, 33648] ] // 自动添加汇总行
    ]
}

3.2.8 模糊查询

qsearch
格式是字段1,字符2,...:查询内容(使用英文逗号及冒号分隔), 表示在指定的若干字段中模糊查询。

示例:在dscr或cmt字段中,查找以“张”开头,并且包含“退款”的订单记录

callSvr("Ordr.query", {qsearch: "dscr,cmt:张* 退款"})

字段可以使用虚拟字段。

查询内容是一个字符串,或多个以空格分隔的字符串。例如“aa bb”表示字段包含“aa”且包含“bb”。 每个字符串中可以用通配符*,如a*表示以a开头,*a表示以a结尾,而*a*a是效果相同的。

3.3 查询分页机制

如果一个查询支持分页(paging), 则一般调用形式为

Ordr.query(page/pagekey?, pagesz/rows?=20) -> {nextkey, total?, @h, @d}

【参数】

pagesz或rows
Integer. 这两个参数含义相同,均表示页大小,默认为20条数据。
page
Integer. 可选,指定分页页码,默认为1(第1页)。
pagekey
Integer. 与page参数指定页码不同,pagekey是另一种基于主键的分页。一般首次查询时不填写(或填写0,表示需要返回总记录数即total字段),而下次查询时应根据上次调用时返回数据的“nextkey”字段来填写。

【查询返回字段】

nextkey
Integer. 一个字符串, 供取下一页时填写参数“pagekey”或“page”。如果不存在该字段,则说明已经是最后一批数据。
total
Integer. 返回总记录数,仅当“pagekey”指定为0时返回,或是指定“page”参数时也会返回。)
h/d
两个数组。实际数据表的头信息(header)和数据行(data),符合压缩表对象的格式。

【示例】

基于page页码的查询较容易理解,常用于管理端分页列表。而pagekey是基于主键的查询,常用于移动端上拉自动加载下一页的列表,示例如下。

第一次查询

callSvr("Ordr.query")

返回

{nextkey: 10800910, h: ["id", "desc", ...], d: [...]}

其中的nextkey将供下次查询时填写pagekey字段;

要在首次查询时返回总记录数,可以设置用pagekey=0:

callSvr("Ordr.query", {pagekey:0})

这时返回

{nextkey: 10800910, total: 51, h: ["id", ...], d: [...]}

total字段表示总记录数。由于缺省页大小为20,所以可估计总共有51/20=3页。

第二次查询(下一页)

callSvr("Ordr.query", {pagekey:"10800910"});

返回

{nextkey: 10800931, h: [...], d: [...]}

仍返回nextkey字段说明还可以继续查询,

再查询下一页

callSvr("Ordr.query", {pagekey: "10800931"})

返回

{h: [...], d: [...]}

返回数据中不带“nextkey”属性,表示所有数据获取完毕。

【分页实现】

分页有两种实现方式:按主键字段的分段查询式分页,以及使用LIMIT操作为核心的传统分页。

分段查询的原理是利用主键id进行查询条件控制(自动修改WHERE语句),每次返回的pagekey字段实际是上次数据的最后一个id.

首次查询:

callSvr("Ordr.query")

SQL样例如下:

SELECT * FROM Ordr t0
...
ORDER BY t0.id
LIMIT {pagesz}

再次查询

callSvr("Ordr.query", {pagekey: "10800910"})

SQL样例如下:

SELECT * FROM Ordr t0
...
WHERE t0.id>10800910
ORDER BY t0.id
LIMIT {pagesz}

分段查询性能高,更精确,不会丢失数据。但它仅适用于未指定排序字段(无orderby参数)或排序字段是id的情况(例如:orderby=“id DESC”)。 查询引擎应根据orderby参数自动选择分段查询或传统分页。

传统分页常通过SQL语句的LIMIT关键字来实现。pagekey字段实际是页码。其原理是:

首次查询

callSvr("Ordr.query", {orderby:"comeTm DESC"})

(以comeTm作为排序字段,无法应用分段查询机制,只能使用传统分页。)

SQL样例如下:

SELECT * FROM Ordr t0
...
ORDER BY comeTm DESC, t0.id
LIMIT 0,{pagesz}

再次查询

callSvr("Ordr.query", {pagekey:2})

SQL样例如下:

SELECT * FROM Ordr t0
...
ORDER BY comeTm DESC, t0.id
LIMIT ({pagekey}-1)*{pagesz}, {pagesz}

3.4 批量导入数据

标准接口Obj.batchAdd用于批量导入数据(支持不存在则添加,存在则更新)。返回导入记录数cnt及编号列表idList:

Obj.batchAdd(title?, uniKey?, useColMap?)(...) -> {cnt, @idList}

它在一个事务中执行,一行出错后立即失败返回,该行前面已导入的内容也会被取消(回滚)。

3.4.1 重复数据处理策略 / uniKey

示例:导入订单,以createTm字段为唯一索引,检查记录是否存在。

策略1:记录不存在则添加,存在则更新:

var data = {list: [
    { createTm: "2022-05-11 07:29:14", dscr: "dscr1" },
    { createTm: "2022-05-11 02:28:40", dscr: "dscr2" },
]};
callSvr("Ordr.batchAdd", {uniKey: "createTm"}, $.noop, data);

策略2:记录不存在则添加,存在则忽略(不更新):

callSvr("Ordr.batchAdd", {uniKey: "createTm", uniKeyMode: "ignore"}, $.noop, data);

策略3:记录不存在则添加,存在则报错。一旦报错则整体导入失败,用户须手工修复数据后重新导入:

callSvr("Ordr.batchAdd", {uniKey: "createTm", uniKeyMode: "error"}, $.noop, data);

策略4:(批量更新)记录存在则更新,不存在则报错(不添加):

callSvr("Ordr.batchAdd", {uniKey: "createTm!"}, $.noop, data);

策略5:(批量更新-不报错)记录存在则更新,不存在则忽略(不添加):

callSvr("Ordr.batchAdd", {uniKey: "createTm!", uniKeyMode: "ignore"}, $.noop, data);

导入支持多种数据格式,详见下节。

3.4.2 批量导入支持三种方式

  1. 直接在HTTP POST中传输内容,数据格式为:首行为标题行(即字段名列表),之后为实际数据行。 行使用“”分隔, 列使用"或逗号分隔(后端自动判断),方便直接从Excel中拷贝数据出来,或导出csv格式文件。 接口为:

    {Obj}.batchAdd(title?)(标题行,数据行) (Content-Type=text/plain)

前端JS调用示例:

var data = "name\taddr\n" + "门店1\t地址1\n门店2\t地址2\n";
callSvr("Store.batchAdd", function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data, {contentType:"text/plain"});

如果不指定contentType,后端一般也能兼容。最好是指定。

或指定title参数:

var data = "门店名\t地址\n" + "门店1\t地址1\n门店2\t地址2\n";
callSvr("Store.batchAdd", {title: "name,addr"}, function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data, {contentType:"text/plain"});

示例: 在chrome console中导入数据

callSvr("Vendor.batchAdd", {title: "-,name, tel, idCard, addr, picId"}, $.noop, `编号 姓名  手机号码    身份证号    通讯地址    身份证图
112 郭志强 15384811000 150221199211215XXX  地址1 532
111 高长平 18375991001 500226198312065XXX  地址2 534
`, {contentType:"text/plain"});
    
  1. 标准csv/txt文件上传:

上传的文件首行当作标题列,如果这一行不是后台要求的标题名称,可通过URL参数title重新定义。 一般使用excel csv文件(编码一般为gbk),或txt文件(以"分隔列)。 接口为:

{Obj}.batchAdd(title?)(csv/txt文件)
(Content-Type=multipart/form-data, 即html form默认传文件的格式)

后端处理时, 将自动判断文本编码(utf-8或gbk).

前端HTML:

<input type="file" name="f" accept=".csv,.txt">

前端JS示例:

var fd = new FormData();
fd.append("file", frm.f.files[0]);
callSvr("Store.batchAdd", {title: "name,addr"}, function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, fd);

在chrome控制台上调用示例:按id匹配已有记录批量更新:

// 用逗号分隔,即csv
callSvr("Ordr.batchAdd", {uniKey: "id!"}, $.noop, `id,dscr
145,描述1
146,描述2`, {contentType:"text/plain"})

// 或用tab分隔,即txt
callSvr("Ordr.batchAdd", {uniKey: "id!"}, $.noop, `id   dscr
145 描述1
146 描述2`, {contentType:"text/plain"})

数据首行是标题,用tab分隔或逗号分隔均可以,但须与内容部分一致。

批量更新时若记录不存在则会失败报错,若不想出错可设置参数uniKeyMode:"ignore"

  1. 传入对象数组

格式为 {list: […]},示例:

var data = {
    list: [
        {name: "郭志强", tel: "15384811000"},
        {name: "高长平", tel: "18375991001"}
    ]
};
callSvr("Store.batchAdd", function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data);

3.4.3 通过导入实现批量更新

batchAdd接口配合标准add接口支持的uniKey参数,可实现存在则更新,不存在则添加的逻辑。

示例:接上节示例,在导入时希望实现根据名称与电话(name和tel字段)匹配,则记录存在则做更新,不存在则添加,只须增加uniKey参数:

callSvr("Store.batchAdd", {uniKey: "name,tel"}, function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data);

3.4.4 支持带子表导入

示例:有以下主-子表对象:

工单:@Ordr: id, code, itemId, qty
工单配料单 @BOM: id, orderId, code, name

注意:拷贝到Excel中看的比较清楚;为避免Excel将长数字显示为科学计数法,在复制前先设置单元格格式为文本。

生产订单号   物料编码    物料规格    开工日期    完工日期    生产数量    子件编码    子件规格    基本用量
SCDD210202302   30101001010484  热像仪#Fotric 615C-L47 2021-02-04  2021-02-04  1.00    20901001000052  标品#Lantern_B31-L47  1
SCDD210202302   30101001010484  热像仪#Fotric 615C-L47 2021-02-04  2021-02-04  1.00    10205001000017  标签#Lantern_40*30mm铜版纸空白标签#中性#通用 1

调用示例:

callSvr("Ordr.batchAdd", {title: "code,itemCode,itemName,planTm,planTm1,qty,@bom.code,@bom.name,@bom.qty", uniKey: "code"}, $.noop, data);

注意:由于子表分布在多行,必须以uniKey参数指定主表唯一字段(支持多个字段联合,以逗号分隔),将根据此字段将多行数组组合成对象后一次导入。 为了正确将主-子表结构的数据行组合成对象,必须保证:组成一个对象的所有行必须在一起,具有相同的uniKey字段,或是对象的第二行起,不指定uniKey字段。

上例也可以简化定义成:(第二行起,无须主表字段,只需要最后三个子表字段) (拷贝到Excel中看)

生产订单号   物料编码    物料规格    开工日期    完工日期    生产数量    子件编码    子件规格    基本用量
SCDD210202302   30101001010484  热像仪#Fotric 615C-L47 2021-02-04  2021-02-04  1.00    20901001000052  标品#Lantern_B31-L47  1
                        10205001000017  标签#Lantern_40*30mm铜版纸空白标签#中性#通用 1

3.4.5 支持列名映射

数据表导入时,默认是按固定列顺序来确定字段的,比如第1列必须是code,第2列必须是itemCode,如果要跳过一列,须通过“-”来指定; 使用列名映射是另一种方式(通过指定参数useColMap=1激活),示例:

id  name    code    itemId  itemCode
1   name1   code1   101 item-101
2   name2   code2   102 item-102

batchAdd调用参数为: {title: "code,itemCode", useColMap:1}

这时只通过列名来匹配(若找不到匹配列则报错!),列的顺序对导入就没有影响。可以通过->来指定列的别名,示例:

编号  物料名 编码  物料名 物料编码
1   name1   code1   101 item-101
2   name2   code2   102 item-102

batchAdd调用参数为: {title: "编码->code,物料编码->itemCode", useColMap:1}。支持子对象列名映射,如子件编码->@bom.code

3.5 子表的增删改查操作

假设主对象为Obj,子对象为Obj1,设计如下:

@Obj: id, name
vcol: @obj1 (说明:vcol表示虚拟字段,@obj1表示字段obj1是个数组,一般就是子对象)

@Obj1: id, objId, name (通过objId关联主对象)

3.5.1 子表添加

在添加主对象时,同时添加子对象:

Obj.add()(name, @obj1...) -> id

示例:

callSvr("Obj.add", $.noop, {
    name: "name1",
    obj1: [
        { name: "obj1-name1" },
        { name: "obj1-name2" }
    ]
});

3.5.2 子表查询

主对象添加后,可以通过get接口获取主对象及子对象:

callSvr("Obj.get", {id: 1001, res:"id,name,obj1"})
返回
{
    id: 1001,
    name: "name1",
    obj1: [
        { id: 10001, name: "obj1-name1" },
        { id: 10002, name: "obj1-name2" }
    ]
}

要控制子对象的查询结果字段,可以加res_{子对象名}参数;要控制子对象的查询参数,可以加param_{子对象名}参数,示例:

callSvr("Obj.get", {id: 1001, res:"id,name,obj1", res_obj1:"id,name"})
或
callSvr("Obj.get", {id: 1001, res:"id,name,obj1", param_obj1: { res: "id,name"} })
callSvr("Obj.get", {id: 1001, res:"id,name,obj1", param_obj1: { res: "id,name", cond: "id>=10002"} })

注意:如果使用了别名,则指定res,param时也要用别名:

callSvr("Obj.get", {id: 1001, res:"id,name,obj1 objList", res_objList:"id,name"})
// 甚至可以多别名分别指定:
callSvr("Obj.get", {id: 1001, res:"id,name,obj1 objList,obj1 objList2", res_objList:"id,name", res_objList2:"id,code"})

当然,也可以直接查询子对象,如:

callSvr("Obj1.query", {cond: "objId=1001", res:"id,name,obj1", fmt:"array"})
返回
[
    { id: 10001, name: "obj1-name1" },
    { id: 10002, name: "obj1-name2" }
]

这里用fmt参数指定返回array格式,因为默认返回的是h/d格式.

3.5.3 子表更新与删除

主对象添加后,可以通过set接口添加/更新/删除子对象。假定后端提供如下更新接口(可更新主表字段name等,子表名为obj1):

Obj.set(id)(name?, @obj1...)

示例:

callSvr("Obj.set", {id: 1001}, $.noop, {
    name: "name1",
    obj1: [
        { id: 10001, name: "obj1-name1-changed" }, // set接口中指定子表id的,表示更新该子表行
        { name: "obj1-name3" },  // set接口中未指定子表id的,表示新增子表行
        { id: 10002, _delete: 1}  // set接口中指定子表id且设置了`_delete: 1`,表示删除该子表行
    ]
});

注意:主对象删除时(del/delIf接口),子对象不会自动删除。后端应根据情况自行处理。

对子表的更新有patch/put两种模式,通过submode参数指定,该参数只对主表set接口有效:

与上述示例中效果相同的操作示例:

// submode=put模式
callSvr("Obj.set", {id: 1001, submode: "put"}, $.noop, {
    name: "name1",
    obj1: [
        { id: 10001, name: "obj1-name1-changed" }, // set接口中指定子表id的,表示更新该子表行; 也可以不指定id,则原来记录被删除,这条会被重新添加。
        { name: "obj1-name3" },  // set接口中未指定子表id的,表示新增子表行
        // 原表中的10002项未指定,则自动被删除。
    ]
});

注意:add接口在指定uniKey参数时,可检查数据存在则更新(即调用set接口)。因此add/batchAdd接口也可以指定submode参数。 在批量导入(batchAdd接口+uniKey参数)时,默认使用put模式做子表更新。

4 批请求

BQP协议支持批请求,即在一次请求中,包含多条接口调用。 而且支持向前引用,即后面的调用可以引用前面调用的返回值。 而且在创建批请求时,可以指定这些调用是否在一个事务(transaction)中,一起成功提交或失败回滚。

假如某场景需要两个请求,先获取用户信息(User.get接口),然后上传页面名、用户编号等信息到服务器(ActionLog.add接口)供统计分析,调用示意如下:

User.get(res="id,name,phone") -> {id, name, phone}
ActionLog.add()(page=home, ver=android, userId={上一调用User.get返回的id}) -> logId

其中,调用二中参数userId需要引用调用一的返回结果。 如果想通过减少调用次数优化性能,可通过批请求,一次性提交两个调用,以及获得每个调用的返回值。

批请求使用接口名“batch”,通过JSON格式传递数据,请求示例如下:

POST /api/batch
Content-Type: application/json;charset=utf-8

[
    {
        "ac": "User.get",
        "get": {"res": "id,name,phone"}
    },
    {
        "ac": "ActionLog.add",
        "post": {"page": "home", "ver": "android", "userId": "{$-1.id}"},
        "ref": ["userId"]
    }
]

请求数据是一个数组,数组中每一项为一个调用,其格式为: {ac, %get?, %post?, @ref?}, 只有ac参数必须,其它均可省略。

POST参数userId的值“{$-1.id}”表示取上一次调用值的id属性。使用向前引用的参数,必须在“ref”参数中指定。

注意:引用表达式应以“{}”包起来,“$n”中n可以为正数或负数(但不能为0),表示对第n次或前n次调用结果的引用,以下为允许的格式:

"{$1}"  -- 第一个调用的返回值
"{$-1}"  -- 前一个调用的返回值
"id={$1.id}"
"{$-1.d[0][0]}"
"id in ({$1}, {$2})"
"diff={$-2 - $-1}"

花括号中的内容将用计算后的结果替换。如果表达式非法,将使用“null”值替代。

batch的返回内容是多条调用返回内容组成的数组,样例如下:

[0, [
    [ 0, {"id": 1, "name": "用户1", "phone": "13712345678"} ],  // 调用User.get的返回结果
    [ 0, 99 ]  // 调用ActionLog.add的返回结果logId
]]

批量请求支持事务(transaction)。

如果批量请求在一个事务中,则最终所有调用会一起成功提交或失败回滚。 要使用事务,只需要请求加个URL参数useTrans=1

POST /api/batch?useTrans=1

5 服务端信息反馈/X-Daca头

BQP协议规定,以下服务端信息应通过HTTP头反馈给客户端。

【接口服务版本号与前端应用热更新】

服务端接口的版本号如果可以获取,应发送给客户端:

X-Daca-Server-Rev: {value}

其中value为最多6位的字符串。

前端应用程序可依据此信息实现热更新: 假如某前端H5应用(或以H5应用为内核的手机原生应用)操作期间,后端接口服务刚好升级过,应用程序再请求时,可以依据版本号变更发现升级行为,从而自动刷新到新版本。

【测试模式和模拟模式】

如果服务运行于测试模式或模拟模式,应设置:

X-Daca-Test-Mode: {value}
X-Daca-Mock-Mode: {value}

其中value为非0值,一般设置为1.

前端应用程序在发现接口服务运行在测试模式时,应予以提示。