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"}]

1 接口通讯协议

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

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

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

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

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

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

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

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

如果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语句。 调试信息仅在测试模式下生效。

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

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}]. 订单中的物品。

注意:

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

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