需求和设计目标:
概念:
注:以下标记*
表示不需要开发者修改的框架实现源文件或工具。
[根目录]
产品设计文档包括:
[后端应用 - server目录]
Ordr.query
可访问http://myserver/mysvc/api.php/Ordr.query
;该文件包含其它实现文件,以及应用内共享的数据。其它应用可包含它从而直接以内部调用方式访问API接口。
框架实现部分:
[工具 - tool目录]
[回归测试 - rtest目录]
内部实现部分:
[前端应用]
如果有筋斗云前端应用,一般放置在以下目录:
[部署环境]
[开发环境]
[演示版]
演示版用于快速开发原型及测试演示,对运行环境低要求,部署极其简单。
在应用设计时,一般使用前缀名为"P_"的环境变量供使用时扩展。
一般用在浏览器中打开URL tool/init.php
进行配置。之后,可手工修改配置文件php/conf.user.php
。一般使用putenv设置环境变量。
[数据库连接]
例:
putenv("P_DB=localhost/mysvc");
putenv("P_DBCRED=bGo6aWhxZ19VR0xH");
[URL路径]
如部署路径为 http://myserver.com/mysvc/ ,则应设置
putenv("P_URL_PATH=/mysvc");
如果不设置,应用将自动判断(但如果服务器上使用了符号链接,则会判断失误)。
注意:
常用环境变量如下:
[测试模式 - TEST_MODE]
X-Daca-Test-Mode: 1
_debug
参数设置调试等级。 如果想要查看本次调用涉及的SQL语句,可以用_debug=9
。[模拟模式 - MOCK_MODE]
这时,对外部系统的依赖(如短信模块,微信接口,支付宝支付等)都将模拟运行,只生成日志到 ext.log 中。
可通过工具 tool/log.php 查看日志。
变量/函数名/数据库表的字段名使用驼峰式(首个单词小写,其余单词首字母大写),如getCarModel, svcId。
WebAPI的调用名和传入参数也采用驼峰式,由于目前实现时不区分大小写, 所以调用时也可以用全部小写(习惯上,url里常常只用小写字母,且不带下划线。) 例如,调用接口名queryOrder,传入参数为modelId, storeId等。
类名,或数据库表名,用大驼峰式(或叫Pascal命名,所有单词首字母大写), 如OrderStatus.
根据[系统建模]设计数据库表结构。
[通用规则]
字段名的类型可在字段后标示,例如:status(2)
表示2字符长度的字符串(nvarchar(2)), 创建时间(dt)
表示date类型。
标记 | 类型 |
---|---|
s | small string(20) |
l | long string(255) |
t | text(64K) |
tt | mediumtext(16M) |
i | int |
n | numeric(decimal) |
date | date |
tm | datetime |
flag | tiny int |
数字 | 指定长度的string |
不指定 | 自动判断 |
如果未指定类型,则根据命名规范自动判断,比如以id结尾的字段会被自动作为整型创建,以tm结尾会被当作日期时间类型创建,其它默认是字符串(长度50),规则如下:
规则 | 类型 |
---|---|
以"Id"/"Cnt"结尾 | Integer |
以"Price"/"Total"/"Qty"/"Amount"结尾 | Currency |
以"Tm"/"Dt"结尾 | Datetime/Date |
以"Flag"结尾 | TinyInt(1B) NOT NULL |
例如,"total", "docTotal", "total2", "docTotal2"都被认为是Currency类型(字段名后面有数字的,判断类型时数字会被忽略)。
也可以用一个类型后缀表示,如 retval&
表示整型,规则如下:
后缀 | 类型 |
---|---|
& | Integer |
@ | Currency |
# | Double |
! | Float |
注意: - 一些名字由于与某些数据库系统关键字冲突应避免,如不使用"desc", 改用"dscr" (description).
TODO: define unique-key, index, not null, default value
客户端通过HTTP协议与服务端交互,调用服务端WebAPI。
Request一般使用HTTP GET/POST方法,如方法描述"fn(p1, p2)"可以用HTTP GET请求(通过URL传参)实现: GET /api.php/fn?p1=value1&p2=value2
, 也可以用POST请求实现:
POST /api.php/fn
Content-Type: application/x-www-form-urlencoded
p2=value2&p1=value1
参数未加说明的, 可以选择通过URL或POST传参.
少数方法描述为"fn(p1)(p2,p3)", 它表示后一个括号中的参数表示必须通过POST传参, 而前一个括号的参数必须用URL传参数, 如:
POST /api.php/fn?p1=value1
Content-Type: application/x-www-form-urlencoded
p2=value2&p3=value3
注意Content-Type需要设置正确, 少数例外情况会特别指出,比如upload方法,它使用"Content-type: multipart/form-data"。
[0, data]
,其中data
的类型由WebAPI返回类型所定义。[非0错误码, 错误信息]
.从返回数组的第3个元素起, 为调试信息, 仅用于问题诊断, 不适合显示给用户看.
所有交互内容采用UTF-8编码。
以下面的WebAPI描述为例:
根据id取车型信息:
getModel(id) -> {id, name, dscr}
它包含以下信息:
WebAPI名称是getModel,参数为id,对应的Request URL为 GET /api.php/getModel?id=100
URL参数ac
表示WebAPI名称,一般用全小写。 为防止服务端缓冲,一般请求时还应加上一个随机参数,如 GET /api.php/getModel?id=100&rnd=5234762234
. 之后的请求示例中, HTTP请求将被简化描述为:
getModel(id=100)
处理成功返回类型为{id, name, dscr}
,例如{id: 100, name: "myname", dscr:"mydscr"}
,关于返回类型表述方式详见下节描述。完整的返回内容为
HTTP/1.1 200 OK
[0, {id: 100, name: "myname", dscr:"mydscr"}]
之后的示例中,返回内容将被简化描述为:
{id: 100, name: "myname", dscr:"mydscr"}
处理失败时返回信息如
HTTP/1.1 200 OK
[1, "未认证"]
[错误码定义]
常用错误码如下:
const E_OK=0;
const E_PARAM=1;
const E_AUTH=2;
const E_DB=3;
const E_SERVER=4;
const E_FORBIDDEN=5;
$ERRINFO = [
E_PARAM => "参数不正确",
E_AUTH => "未认证",
E_DB => "数据库错误",
E_SERVER => "服务器错误",
E_FORBIDDEN => "禁止操作"
];
[关于空值]
假如有参数"a=1&b=&c=hello", 其中参数"b"值为空串。 一般情况下,被当作未赋值处理,即与"a=1&c=hello"意义相同。
只有在对象保存上下文中(典型的是通用对象接口的set操作),且出现中POST内容的"a="表示将该字段置null(与"a=null"语义相同), 注意:不是置空字符串。
{id, name}
一个简单对象,有两个字段id和name。e.g. {id: 100, name: "name1"}
[id...] or [id]
一个简单数组,元素为id。e.g. [100, 200, 400]
, 每项为一个id
[id, name]
一个简单数组,e.g. [100, "liang"]
,第一项为id, 第二项为name
[ [id, name] ] 或 varr(id, name)
简单二维数组,又称varr, 如 [ [100, "liang"], [101, "wang"] ]
.
[{id, name}] 或 objarr(id, name)
一个数组,每项为一个对象,又称objarr。e.g. [{id: 100, name: "name1"}, {id: 101, name: "name2"}]
tbl(id, name)
table对象。其详细格式为 {h: [header1, header2, ...], d:[row1, row2, ...]}
,例如
{
h: ["id", "name"],
d: [[100, "myname1"], [200, "myname2"]]
}
table对象支持分页机制(paging),返回字段中包含"nextkey"等。 详情请参考下一章节"分页机制".
注意:
[可选参数]
如果API的参数表示为:
fn(p1, p2?, p3?=1)
它表示:
[复杂类型序列化为字符串描述]
有时用一个字符串字段表示复杂的结构,这时常以下类型描述方式:
逗号分隔的简单字符串序列,如
"经度,纬度"
或
"经度/Double,纬度/Double"
可表示121.233543,31.345457
特别地,Coord类型: Coord="经度/Double,纬度/Double".
逗号分隔行,冒号分隔列的表,简称list,如
"id:name?,"
参数后加"?"表示是可选参数(逗号不可少,表示数组,即后面可有多个重复项)
或
list(id, name?)
或指定类型
list(id/Integer, name?/String)
每个元组用","分隔, 元组内每个字段用":"分隔。每个字段内不能有这两个特殊符号(如果是日期,中间不可以有":", 如"2015/11/20 1030"或"20151120 1030")。 例如
10:liang,11:wang
如果name字段省略,则可简化为10,11
.
TODO: 也可以带表头信息(首字符"@"标明有表头),如 @id:name,10:liang,11:wang
或 @id,10,11
.
这种格式一般用于传递简单的表。更加复杂的类型可使用json格式传递。
换行符""分隔行, 制表符""分隔列的表, 称为table,可表示为 "id \t name \n"
或 table(id, name)
如果一个查询支持分页(paging), 则一般调用形式为
Ordr.query(pagekey?, pagesz?=20) -> {nextkey, total?, @h, @d}
或
Ordr.query(page, rows?=20) -> {nextkey, total, @h, @d}
[参数]
[返回值]
[示例]
第一次查询
Ordr.query()
返回
{nextkey: 10800910, h: [id, ...], data: [...]}
其中的nextkey将供下次查询时填写pagekey字段;首次查询还会返回total字段。由于缺省页大小为20,所以可估计总共有51/20=3页。
要在首次查询时返回总记录数,则用pagekey=0:
Ordr.query(pagekey=0)
这时返回
{nextkey: 10800910, total: 51, h: [id, ...], data: [...]}
第二次查询(下一页)
Ordr.query(pagekey=10800910)
返回
{nextkey: 10800931, h: [...], d: [...]}
仍返回nextkey字段说明还可以继续查询,
再查询下一页
Ordr.query(pagekey=10800931)
返回
{h: [...], d: [...]}
返回数据中不带"nextkey"属性,表示所有数据获取完毕。
分页有两种实现方式:分段查询和传统分页。
分段查询性能高,更精确,不会丢失数据。但它仅适用于未指定排序字段(无orderby参数)或排序字段是id的情况(例如:orderby="id DESC")。 系统将根据orderby参数自动选择分段查询或传统分页。
[分段查询]
分段查询的原理是利用主键id进行查询条件控制(自动修改WHERE语句),pagekey字段实际是上次数据的最后一个id.
首次查询:
Ordr.query()
SQL样例如下:
SELECT * FROM Ordr t0
...
ORDER BY t0.id
LIMIT {pagesz}
再次查询
Ordr.query(pagekey=10800910)
SQL样例如下:
SELECT * FROM Ordr t0
...
WHERE t0.id>10800910
ORDER BY t0.id
LIMIT {pagesz}
[传统分页]
传统分页只需要通过SQL语句的LIMIT关键字来实现。pagekey字段实际是页码。其原理是:
首次查询
Ordr.query(orderby="comeTm DESC")
(以comeTm作为排序字段,无法应用分段查询机制,只能使用传统分页。)
SQL样例如下:
SELECT * FROM Ordr t0
...
ORDER BY comeTm DESC, t0.id
LIMIT 0,{pagesz}
再次查询
Ordr.query(pagekey=2)
SQL样例如下:
SELECT * FROM Ordr t0
...
ORDER BY comeTm DESC, t0.id
LIMIT ({pagekey}-1)*{pagesz}, {pagesz}
服务端支持HTTPS服务。客户端默认应使用HTTP协议与服务器通信;对于个别敏感的API,如涉及用户密码的登录、注册、修改密码等操作,应使用HTTPS协议进行通信。
注意:
在服务端配置为测试模式时,可用特殊的URL参数"_debug"定义调试等级, 默认为0. 如果为1-9的数字, 将添加调试信息到结果数组中.
_debug=9: 输出SQL
通过设置环境变量P_DEBUG, 可以让本系统使用的测试工具(如client.php及rtest.php)请求时指定调试等级. 如:
set P_DEBUG=9
client.php callsvr usercar.query
特殊的URL参数"_app"用于定义当前应用. 缺省值为"user"(对应客户端应用). 注意: 每个请求都必须带此标识, 它决定session对应的cookie项的名字. 如果不加该参数, 则可能出现未预料的错误.
常用应用标识如下:
由于不同应用(如客户端, 商户端与管理端)共用一个api.php页面, 当在同一浏览器中打开多个应用时可能相互影响, 比如商户端与管理端同时使用时, 商户端会自动以管理端的权限操作.
解决方法是, 为不同应用使用不同的sessionId以区分. 实现时有两种方式, 一是访问不同的服务端页面, 如商户端访问api.php, 管理端访问api0.php(其中指定一个不同的sessionId); 另一种是商户端所有的请求都带一个参数指定sessionId. 我们将采用后一种解决方法.
URL参数_app可用于指定所属应用, 如不指定默认值为"user". 它隐含着session的名称为"{_app}id"如请求
GET /api.php?_app=emp
第一次访问将返回
SetCookie: empid=xxxxxx
以下URL等价:
http://localhost/mysvc/api.php/login?phone=137&pwd=1234
->
http://localhost/mysvc/api.php?ac=login&phone=137&pwd=1234
对于对象的CRUD,URL像这样:
http://localhost/mysvc/api.php/Ordr.query?res=id,dscr
->
http://localhost/mysvc/api.php?ac=Ordr.query&res=id,dscr
URL参数"_ver"值为客户端版本。取值参考表ApiLog.ver字段。目前只有安卓客户端设置该参数为"a/{ver}" (如"a/2"), 其它客户端版本根据userAgent自动获取。
要访问每个API,必须定义相应的权限。权限中包括登录类型一般用AUTH_XXX表示,一般权限用PERM_XXX表示。
员工登录后可能获得以下一个或多个权限: - PERM_MGR: 操作商户的所有内容, 包括订单, 物料, 员工等(但不能设置员工权限).
其它权限: - PERM_TEST_MODE: 测试模式下可用。 - PERM_MOCK_MODE: 模拟模式下可用。
以下接口提供对象的基本增删改查(CRUD)以及列表查询、统计分析、导出等服务,为通用对象接口原型,在设计时应以此为基础形式,再按业务逻辑定义专用接口,描述权限和业务逻辑等。 下面用Obj代指对象实际名称。
添加:
Obj.add()(POST fields...) -> id
Obj.add(res?)(POST fields...) -> {fields...} (由res指定)
更新:
Obj.set(id)(POST fields...)
获取:
Obj.get(id, res?) -> {fields...}
删除:
Obj.del(id, force?)
查询列表(默认压缩表格式):
Obj.query(res?, cond?, distinct?=0, pagesz?=20, pagekey?, fmt?) -> tbl(fields...) = {nextkey?, total?, @h, @d}
查询列表 - 对象列表格式:
Obj.query(fmt=list, ...) -> {nextkey?, total?, @list=[obj1, obj2...]}
后端内部查询支持以下额外参数:@res2?, join?, @cond2?, union?, subobj
分组统计:
Obj.query(gres, pivot?, pivotCnt?...) -> tbl(fields...)
导出文件:
Obj.query(fmt=csv/txt/excel, fname?...) -> 文件内容
批量更新:
Obj.setIf(cond)(POST fields...)
批量删除:
Obj.delIf(cond)
批量添加(导入):
Obj.batchAdd(title?)(数据或文件)
缺省这些操作只对超级管理员角色开放,其他角色默认无权操作。超级管理员可对所有对象的所有字段操作。
对象是否开放出来,或是开放哪些操作及字段,一般按用户角色进行权限控制,如用户登录后可操作某些对象,或员工登录后可操作另一些对象,请查阅设计文档中相应的专用接口定义。 在专用接口定义中应描述允许的角色、允许的操作类型(如只能get/set,不能add/del)、只读字段、隐藏字段等,以及应实现的业务逻辑。
系统中的表设计(表名, 字段等)参见"数据库设计"章节. 一般表都设计为使用整形字段id作为主键. 该字段创建后变只读, 不允许被修改.
对于add操作,默认返回id, 如果想多返回其它字段,可设置res参数,如
Ordr.add()(status="CR", total=100) -> 809
Ordr.add(res="id,status,total")(status="CR", total=100) -> {id: 810, status:"CR", total: 100}
query可以用参数cond指定查询条件, 如
cond="type='A' and name like '%hello%'"
URL编码后为
cond=type%3d%27A%27+and+name+like+%27%25hello%25%27
query返回支持多种形式, 缺省返回压缩表类型如:
{
"h": ["id", "name"],
"d": [[1, "liang"], [2, "wang"]]
}
如果指定fmt=list
则返回:
{
"list": [{"id": 1, "name": "liang"}, {"id": 2, "name": "wang"}]
}
注意:
[分页]
分页只适用于query接口,详细请参考章节"分页机制".
[参数]
特别地,res支持枚举列表,即自动将枚举值转化为可读字符串,例如res=id 编号,status 状态=CR:创建;PA:已付款
,这样返回的status字段会自动转换成相应的值。
[限程序内使用的参数]
目前仅限后端内部使用, 要求主对象必须有id字段(未指定res/res2参数或其中有id字段). 格式为数组, 每行指定一个子对象的查询, 每行格式为: {sql, wantOne?}. "sql"指定查询语句, 其中用"%d"表示主表id; "wantOne"表示返回对象而非对象集合(数组), 缺少是对象集合. 例:
$_REQUEST["subobj"] = [ "items" => ["sql"=>"SELECT * FROM OrderItem WHERE orderId=%d", "wantOne"=>false]];
对res, cond, orderby的安全限制:
[导出文件]
[分组统计]
例:统计2015年2月,按状态分类(如已付款、已评价、已取消等)的各类订单的总数和总金额。
Ordr.query(gres="status", res="count('A') totalCnt, sum(amount) totalAmount", cond="tm>='2016-1-1' and tm<'2016-2-1'")
返回内容示例:
[
h: ["status", "totalCnt", "totalAmount"],
d: [
[ "PA", 130, 1420 ], // 已付款,共130单,1420元
[ "CA", 29, 310 ], // 取消的订单
[ "RA", 1530, 15580 ], // 已评价的订单
]
]
例:统计2015年2月,按状态分类(如已付款、已评价、已取消等)的各类订单的总金额,并将状态转置到列上。
Ordr.query(gres="status", res="sum(amount) totalAmount", cond="tm>='2016-1-1' and tm<'2016-2-1'", pivot: "status")
返回内容示例:
[
h: ["PA", "CA", "RA"],
d: [
[ 1420, 310, 15580 ]
]
]
例:统计2015年2月,按状态分类(如已付款、已评价、已取消等)的各类订单的总数和总金额(两个统计列),并将状态转置到列上。
Ordr.query(gres="status", res="count('A') totalCnt, sum(amount) totalAmount", cond="tm>='2016-1-1' and tm<'2016-2-1'", pivot: "status", pivotCnt: 2)
返回内容示例:
[
h: ["PA", "CA", "RA"],
d: [
[ [130, 1420], [29, 310], [1530, 15580] ]
]
]
[操作特殊属性flags和props]
flags为单字母表示的标志位集合,如"vg";props可以以多字母表示标志位,中间以空格分隔,如"suv mpv". 一般flags由应用内部定义;而props扩展性更强。
query操作支持形如flag_{flag}
或prop_{prog}
的虚拟属性。
flag_f=1
相当于 flags LIKE '%f%'
flag_f=0
相当于 flags IS NULL OR flags NOT LIKE '%f%'
get/query操作中如果返回了flags/props,还会返回相应的虚拟属性;例如flags值为"vg",则多返回虚拟属性flag_v=1
及flag_g=1
set操作支持以下方式设置flags/props属性:
flag_f=1
相当于 set flags=concat(ifnull(flags, ''), 'f')
flag_f=0
相当于 set flags=replace(flags, 'f', '')
注意:
[例: 添加商户]
添加商户, 指定一些字段:
Store.add()
name=华莹汽车(张江店)
addr=金科路88号
tel=021-12345678
注:
Store是商户表名, 通过POST字段传递各字段内容. HTTP POST请求如下所示(实际发送时, 每个字段的值应使用UTF8+URL编码, 示例中未进行编码):
POST /api.php/Store.add
Content-Type: application/x-www-form-urlencoded
name=华莹汽车(张江店)&addr=金科路88号&tel=021-12345678
id这种主键或只读字段无须设置. 即使设置也应被忽略.
操作成功时返回id值:
8
[例: 获取商户]
取刚添加的商户(id=8):
Store.get(id=8)
操作成功时返回该行内容:
{id: 8, name: "华莹汽车(张江店)", addr: "金科路88号", tel: "021-12345678", opentime: null, dscr: null}
可以像query方法一样用POST参数res指定返回值, 如
Store.get(id=8
res=id,name as storeName,addr
)
操作成功时返回该行内容:
{id: 8, storeName: "华莹汽车(张江店)", addr: "金科路88号"}
[例: 查询商户]
查询"华胜汽车"在"浦东"的门店, 即查询名称含有"华胜汽车"且地址中含有"浦东"的商户, 只返回id, name, addr字段:
Store.query()
res=id,name,addr
cond=name like '%华胜%' and addr like '%浦东%'
操作成功时返回内容如下:
{
"h": [
"id",
"name",
"addr"
],
"d": [
[
"100064",
"华胜汽车(金桥店)",
"上海市浦东区金桥路2622弄59号3号门"
]
]
}
[导出商户]
Store.query()
res=id,name,addr
fmt=excel
pagesz=9999
可导出gb2312编码的csv文件。使用较大的pagesz以尽量返回所有数据。
[例: 更新商户]
为商户设置描述信息等:
Store.set(id=8)
opentime=8:00-18:00
dscr=描述信息.
操作成功时无返回内容.
[例: 删除商户]
Store.del(id=8)
操作成功时无返回内容.
Obj.setIf/delIf
一般使用cond参数指定查询条件,允许使用虚拟字段;也可以使用与query接口相同的其它自定义参数。
示例,下面query接口定义了虚拟字段storeName和非标查询参数q:
Item.query(q?, cond?) -> tbl(id, ..., storeName?)
- q: "my"-表示我发布的商品. 默认返回所有商品。
- storeName: 商户名,关联到Store表。
批量更新示例:
Item.setIf(cond: "status='CR')(status=ST)
Item.setIf(cond: "t0.id IN (3,5,7)")(status=ST)
Item.setIf(q:"my", cond:"storeName LIKE "上海%")(status=ST)
批量删除示例:
Item.delIf(cond: "status='ST')
Item.delIf(cond: "t0.id IN (3,5,7)")
Item.delIf(q: "my", cond:"storeName LIKE "上海%")
批量添加(导入)。返回导入记录数cnt及编号列表idList
Obj.batchAdd(title?)(...) -> {cnt, @idList}
在一个事务中执行,一行出错后立即失败返回,该行前面已导入的内容也会被取消(回滚)。
支持两种方式上传:
直接在HTTP POST中传输内容,数据格式为:首行为标题行(即字段名列表),之后为实际数据行。 行使用""分隔, 列使用""分隔. 接口为:
{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"});
或指定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"});
上传的文件首行当作标题列,如果这一行不是后台要求的标题名称,可通过URL参数title重新定义。 一般使用excel csv文件(编码一般为gbk),或txt文件(以""分隔列,一般为utf-8编码)。 接口为:
{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);
详细可参考文档 子表对象设计
假设有主表对象Ordr,一对多关联子表对象OrderItem,数据模型如下:
订单:
@Ordr: id, dscr, amount
物料:
@Item: id, name, price
订单明细
@OrderItem: id, orderId, itemId, itemName, price, qty, amount
[在add接口中指定子表项]
Ordr.add()(dscr, amount, @items=[{itemId, price, qty}])
示例:
var items = [{itemId:1, price: 200, qty: 1.0}, {itemId:2, price:100, qty:3.0}];
callSvr("Ordr.add", $.noop, {dscr: "订单1", amount: 500, items: items}, {contentType:"application/json"});
[在get接口中返回子表]
Ordr.get(id, res_items?) -> {id, ..., @items?}
接口返回的items
为OrderItem子表数组内容。调用示例:
var id = 1;
callSvr("Ordr.get", {id: id, res:"*,items"})
返回示例:
{id: 1, dscr: "订单1", amount: 500, items: [
{id: 100, itemId:1, orderId:1, price:200, qty:1, itemName:"item 1", amount:200},
{id: 101, itemId:2, orderId:1, price:100, qty:3, itemName:"item 2", amount:300}
]}
我们知道通过res
参数可指定返回项,而对于通用子表接口,通过res_{子表显示名}
可指定子表项的返回项,在本例中,用参数res_items
可指定子表项items
的返回项,调用示例:
callSvr("Ordr.get", {id: id, res:"id,items", res_items:"id,itemName,amount"})
返回示例:
{id: 1, items: [
{id: 100, itemName:"item 1", amount:200},
{id: 101, itemName:"item 2", amount:300}
]}
示例2,指定字段显示名:
callSvr("Ordr.get", {id: id, res:"id 订单编号,items 订单明细", res_订单明细:"id,itemName 物料,amount 金额"})
返回示例:
{订单编号: 1, 订单明细: [
{id: 100, 物料:"item 1", 金额:200},
{id: 101, 物料:"item 2", 金额:300}
]}
[在set接口中追加、修改、删除子表项]
Ordr.set()(..., items={id?, itemId?, price?, qty?})
注意更新时对子项采用PATCH机制,即不必传所有子项,如果在子项中指定了id,则做更新子项操作(特别地,如果有_delete
参数,表示删除该子项),否则做追加子项操作。
更新子项示例:更新id=100
子项的,数量设置为2,同时更新子表和主表的amount字段
callSvr("Ordr.set", {id: id}, $.noop, {items:[{id:100, qty: 2, amount: 400}], amount:700} , {contentType:"application/json"})
追加子项:追加{itemId:1}
的子项,并更新相关字段
callSvr("Ordr.set", {id: id}, $.noop, {items:[{itemId:1, qty:2, price:50, amount:100}], amount:800} , {contentType:"application/json"})
删除子项:删除id=100
子项,指定_delete
为1
callSvr("Ordr.set", {id: id}, $.noop, {items:[{id:100, _delete:1}], amount:300} , {contentType:"application/json"})
所有对API的调用(请求与响应)均记录到表ApiLog中供分析。
对一个session, 监控其API调用是否有异常,避免自动化操作行为,这时将返回 E_FORBIDDEN 错误。安全类异常将记录到日志文件 secure.log
注意: - 自动化测试时,API监控不工作(这时_test参数值为2)。 - 应避免客户端一次取多张图片时有问题
BQP协议支持批量请求,即在一次请求中,包含多条调用。 在创建批量请求时,可以指定这些调用是否在一个事务(transaction)中,一起成功提交或失败回滚。
前端接口示例:
var batch = new MUI.batchCall();
// var batch = new MUI.batchCall({useTrans: true}); // 使用同一事务时,可指定useTrans=true
// 调用一
var param = {res: "id,name,phone"};
callSvr("User.get", param, function(data) {} )
// 调用二
var postParam = {page: "home", ver: "android", userId: "{$1.id}"};
callSvr("ActionLog.add", function(data) {}, postParam, {ref: ["userId"]} );
batch->commit();
// batch->cancel();
还有一种方式更简单:
MUI.useBatchCall(); // 在本次消息循环中执行所有的callSvr都加入批处理。
// MUI.useBatchCall({useTrans:1}, 20); // 表示20ms内所有callSvr都加入批处理, 且启用事务。
callSvr(...);
callSvr(...);
callSvr(..., {noBatch: 1}); // TODO:使用noBatch参数可以强制单独执行,不加入批处理。
其中,调用二中参数userId引用了调用一的返回结果,通过在callSvr后指定参数ref标明。userId的值"{$1.id}"表示取第一次调用值的id属性。 注意:引用表达式应以"{}"包起来,"$n"中n可以为正数或负数(但不能为0),表示对第n次或前n次调用结果的引用,以下为可能的格式:
"{$1}"
"id={$1.id}"
"{$-1.d[0][0]}"
"id in ({$1}, {$2})"
"diff={$-2 - $-1}"
花括号中的内容将用计算后的结果替换。如果表达式非法,将使用"null"值替代。
数据传输格式:
提交使用JSON格式,示例如下
POST api/batch
[
{
"ac": "User.get",
"get": {"res": "name,phone"}
},
{
"ac": "ActionLog.add",
"post": {"page": "home", "ver": "android", "userId": "{$-1.id}"},
"ref": ["userId"]
}
]
数组中每一项为一个调用,其格式为: {ac, %get?, %post?, @ref?}, 只有ac参数必须,其它均可省略。
如果使用事务,只是URL上加个参数:
POST api/batch?useTrans=1
batch的返回内容是多条调用返回内容组成的数组,样例如下:
[0, [
[ 0, {id: 1, name: "用户1", phone: "13712345678"} ], // 调用User.get的返回结果
[ 0, "OK" ] // 调用ActionLog.add的返回结果
]]
BQP协议规定,以下服务端信息应通过HTTP头反馈给客户端。
服务端API版本号如果可以获取,应发送给客户端:
X-Daca-Server-Rev: {value}
其中value为最多6位的字符串。
如果服务运行于测试模式或模拟模式,应设置:
X-Daca-Test-Mode: {value}
X-Daca-Mock-Mode: {value}
其中value为非0,一般为1.
在完整的APP设计文档中,应包括对前端应用接口的描述。
每一个筋斗云前端应用均应定义一个唯一的应用标识(app),如"emp", "emp-store"等。在调用交互接口时,框架会自动将应用标识作为参数传给后端。 应用标识中的主干部分称为应用类型(app type),例如应用"emp", "emp-store", "emp2"的主干(字符"-"之前的部分,不包括尾部数字),都是相同的应用类型"emp"。 应用类型常用于登录类型与权限控制。
例如,定义客户端应用标识app=user,其应用类型也是"user",在login交互接口中,对应用类型"user"将作用户登录处理(如查询用户表),登录成功后赋予其用户权限。 定义员工端应用标识为app=emp,它的应用类型是"emp",在后端将作员工登录处理(比如查询的是员工表),登录成功后赋予其员工权限。 定义管理端应用app=emp-adm,它与应用emp-store是相同类型,因而登录方式和权限是相同的,即应使用员工信息登录。
可见,不同的应用可以是相同的应用类型。在实现交互接口时,不同的应用标识也会使用不同的cookie名称,以避免多个应用同时使用时相互干扰。
下面以筋斗云前端示例应用为例,介绍常见的前端应用接口描述方式。
筋斗云的移动应用可做为Web应用在浏览器中运行, 也可以接入微信公众号或支付宝服务窗, 也可以通过cordova框架包装在应用容器中提供android/ios应用程序.
移动端应用按惯例放在m2目录下。以用户端为例,其相关文件有:
客户端:
此外还有文件:
移动应用的对外接口包括页面URL,允许的入口页面(entry),URL参数等。下面举例说明前端应用接口的描述方式:
URL地址:
m2/index.html
应用标识为"user"。
[进入移动客户端并显示指定订单]
m2/index.html#order(orderId)
这表示可以请求这样的URL:
m2/index.html?orderId=32#order
其中: m2/index.html是页面地址, "?"后为参数(使用URL编码方式), "#"后为入口点, 表示允许进入的逻辑页面.
上例中的访问表示: 打开移动客户端的订单页面, 参数为orderId=32, 即显示32号订单.
桌面应用按惯例放在web目录下。其常用文件与移动应用类似,以管理端应用为例:
常见的桌面应用示例如下。
一般由商户员工使用,管理员工、订单等:
web/store.html
该应用的应用标识定义为"emp-adm",它与员工端(emp-store)的应用类型是相同的,都是"emp", 因而都用员工信息进行登录。
一般由超级管理员使用,甚至可执行SQL语句:
web/adm.html
该应用的应用标识定义为"admin",使用超级管理员帐号登录。注意:超级管理员帐号在用户配置文件conf.user.php
中由P_ADMIN_CRED
环境变量设定。
在查找对象的对话框中,可支持多种灵活的匹配方式:
例如对字段a, 填写以下值:
hello
(匹配) - 生成查询条件 a='hello'
100
(纯数字匹配) - 生成 a=100
*28*
或 %28%
(部分匹配) - 生成a like '%28%'
>100
/ >=100
/ <100
/ <=100
/ <>100
- 生成 a>100
等null
/ <>null
- 生成 a is null
/ a is not null
empty
/ <>empty
- 生成 a=''
/ a<>''
null,0,1
表示a is null or a=0 or a=1
a-b
方式指定范围,如1-10,20-30,40
, 表示(a>=1 and a<=10) or (a>=20 and a<=30) or a=40
>=100 and <200
, null or 0 or 1
, 1-100 and <>50
,不支持用括号组合条件。如果同时对多个字段填写了搜索值,则表示这些条件需要同时满足,即AND
关系。
详细可参考文档 API参考 -> 筋斗云前端(桌面Web版) -> WUI.getQueryCond
移动应用和桌面应用的框架支持以下通用参数:
以下参数适用于移动应用:
注意: 一旦设置为非0, 则该值会被记住, 下次打开时即使未指定也会有值, 必须重新设置cordova=0清除(或在控制台中调用delStorage("cordova")).
以下参数适用于桌面应用:
移动应用和桌面应用使用以下JS全局变量:
以上变量可通过控制台手工调节部分参数.
注:在Mac OS中一般用Command键替代Ctrl键。
其它隐含用法(一般用于调试):
[测试需求]
所有测试内容存放在rtest
目录下。
环境变量"SVC_URL"可设置使用的URL, 如
> set SVC_URL=http://115.29.199.210/mysvc
> run_rtest.pl all
除可通过浏览器(如Chrome插件Postman)等工具进行测试外,还提供client.php工具,可分别测试每个API,如
> client.php queryseries 100
也可直接调用callsvr方法调用任意api, 例如以下调用等价于前面例子:
> client.php callsvr queryseries brandId=100
再如通用的对表的查询: 格式callsvr command [paramstr] [poststr]
, 其中paramstr
和poststr
应使用URL编码.
(get item with id)
> client.php callsvr item.get id=1
(set item with id)
> client.php callsvr item.set id=1 "price=434&dscr=hehe"
注意:
client.php
可查看支持的API.client.php api1 ?
表示显示api1的帮助.client.php api1 param1 null param3
.[前提条件]
运行服务端和回归测试:
> cd rtest
> run_server.bat
(设置服务URL,也可以手工设置)
> setTestEnv.bat
> set SVC_URL=...
(运行所有case)
> run_rtest.pl all
(运行一个case)
> run_rtest.pl testcase1
日志"rtest.log"记录所有HTTP request和response,用于分析业务逻辑失败的原因。
run_rtest是对phpunit进行了封装的工具,下面介绍。
[run_rtest.pl]
phpunit可以执行多个case, 不能自动分析依赖关系. run_rtest.pl工具就是用于简化对个别Case的测试, 它可运行一个多个或全部测试用例。
(执行所有用例)
> run_rtest.pl all
(执行一个用例, 用例名参考rtest.php中的test系列函数, 名称可忽略大小写; 工具将自动先执行依赖的用例)
> run_rtest.pl testupload
(执行多个用例,工具将自动调整各用例执行顺序)
> run_rtest.pl testatt testupload
下面是一些特殊配置:
通过设置环境变量P_DEBUG, 可以让本系统使用的测试工具(如client.php及rtest.php)请求时指定调试等级. 如:
set P_DEBUG=9
client.php callsvr usercar.query
等级9将打出服务端SQL语句,并且通过自动设置URL参数"XDEBUG_SESSION_START=netbeans-xdebug"触发服务端php调试器(必须安装php-xdebug).
指定app名称(间接指定session名). 对应系统URL参数"_app" (参考章节"应用标识(_app)). 在多种客户端同时登录时用于区分每个会话.
rtest缺省会在测试前后创建和删除cookie, 测试时会自动找一个用户测试, 测试用例中也包括创建新用户.
如果设置了环境变量P_SHARE_COOKIE, 则不会创建和删除新cookie, 而是用当前已登录的用户测试(其中会用到API whoami来确定当前用户).
例如, 你想借助rtest为特定的用户创建订单, 可以这样:
> set P_SHARE_COOKIE=1
> client.php login 13712345678 1234
(登录用户为13712345678)
(用当前登录用户添加order)
> run_rtest.pl testaddorder
(用当前登录用户运行所有测试, 将忽略注册, 登录等接口测试)
run_rtest.pl all
[phpunit用法]
运行所有回归测试:
phpunit rtest.php
phpunit其它常用参数如下:
运行一个或多个Case(注意:名称大小写必须正确,被依赖的case必须先执行)
run_rtest --filter testGeneralQuery
run_rtest --filter testUpload|testAtt
just scenario test:
run_rtest --group scenario
just sanity test:
run_rtest --exclude-group scenario
[原则]
用例测试或场景测试(usecase/scenraio): 通过调用若干API完成一个有意义的场景。
if any critical case fails, ignore the rest.
[实现]
参考file rtest.php:
static private $isIT =false;
static private $skipIT = false;
static private $skipAll = false;
private $isCritical = false;
必要时对某些类进行专门测试,存放在"rtest/test"目录,手工运行它们:
cd test
phpunit xxxTest.php
我们使用的权限控制模型为: "用户-权限(即角色/原子权限组)-原子权限(基本权限)". (注:权限组也称"角色", 所以某些系统中也称为"用户-角色-权限"模型.)
在我们系统中, 用户主要有User和Employee两类.
权限有两类: authorization(或称permission), 以及data ownership. - 前者控制用户可以访问的表及列, 以及对该对象(甚至粒度到字段)可以有哪些操作(读/写). 例如, 在我们系统中, User登录后可以添加订单(Ordr.add, 且只允许设置指定的字段), 不可以删除订单. 实现时, 通过AC1_Ordr类控制了操作类型, 字段等. - 后者控制用户可以访问的行, 例如, 虽然用户可以访问订单对象, 但只能操作自己的订单, 但不能操作别人的订单. 实现时, 在AC1_Ordr类的onValidateId(检查set/del/get操作)/onQuery(检查query操作)中, 均做了控制.
在后台系统中, 对象操作类的原子权限定义是通过AccessControl类簇实现的, 每一个类(如AC1_Ordr)即是一个权限定义(包括了对象权限, 列权限及行权限). 而对于函数操作类权限是通过checkAuth(角色)显示定义: 只有指定角色的用户才能调用.
权限定义参考章节权限说明.
项目初始化步骤:
http://{server}/{app}/tool/init.php
检查php环境是否满足需求。如果需要重新配置,可删除配置文件 php/conf.user.php后再运行本工具。
在配置文件中,很多帐户口令、密码采用base64等方式保存,可以用在线工具 http://{server}/{app}/tool/tool.php
进行编解码。
tool/upgrade.php - 升级管理
[原理]
它根据主设计文档中(DESIGN.wiki)中的数据模型定义,创建或更新数据库表,定义数据模型示例:
@ApiLog: id, tm, addr
主设计文档中可以包含其它设计文档,指令如下: (支持版本: v3.1)
@include sub/mydesign.wiki
如果使用了插件,一般应包含所有插件文档,以便插件中的表也可被创建。在DESIGN.wiki中目前默认就有这样一行:
@include server/plugin/*/DESIGN.wiki
注意:
不会删除表或字段。如有需要请手工操作。通过
upgrade.sh 'export(2)'
导出用于升级的SQL语句,然后使用SQL工具运行即可。或通过登录超级管理端,在其中运行SQL语句(注意非SELECT语句前面要加"!"强制执行)。
对已有的字段,不能修改字段类型。需要手工操作。
[数据库连接]
upgrade.php与api.php配置方式相同,都是通过环境变量来指定,可以使用conf.user.php中的配置,数据库支持mysql和sqlite.
环境变量P_DB可为升级工具指定数据库. 如
环境变量P_DBCRED指定连接数据库的用户名密码。
upgrade.php
缺省进入命令行交互.
一般命令格式与函数调用类似, 也支持直接的sql语句, 如
> addtable("item")
> addtable("item", true)
> quit()
对于无参数命令可不加括号
> quit
支持直接的sql语句
> select * from item limit 10
> update item set price=333 where id=8
[help]
参数: [command]
显示帮助. 可以指定command名称, 全部或部分均可.
例:
> help
> help("addtable")
> help("table")
[initdb]
自动添加所有表. 等同于updatedb命令.
[updatedb]
自动添加或更新所有表. 相当于对所有表调用addtable命令. 如果某张表已存在, 则检查是否有缺失的字段(注意: 只检查缺失, 不检查字段类型是否变化), 有则添加, 否则对该表不做更改.
[execsql]
参数: {sql} [silent=false]
对于select语句, 返回结果集内容; 对于其它语句, 返回affectedRows.
例:
> execsql("select * from item limit 10")
> execsql("update item set price=10 where id=3")
注: - 支持直接输入SQL语句, 会自动调用execsql()执行. 程序通过以select等关键字识别SQL, 如
> select * from item limit 10
> update item set price=10 where id=3
[quit]
退出交互. 可简写为"q".
例:
> quit
或
> q
[export]
v5.1新增. 参数: {type?=0}
[TODO: upgrade]
TODO 自动根据版本差异,执行升级脚本,升级数据库. 如果字段cinf.ver不存在, 则重建DB(但会忽略已有的表, 不会删除它再重新创建). 升级完成后设置cinf.ver字段.
[showtable]
参数: {table?="*", checkDb=false}
查看某表的metadata以及SQL创建语句. 参数{table}中可以包含通配符。
例:
> showtable("item")
> showtable("*log")
checkDb: 根据META与数据库差异,显示用于升级的SQL语句。
showtable(null, true)
[addtable]
参数: {table} [force=false]
根据metadata添加指定的表{table}. 未指定force参数时, 如果表已存在且未指定force=true, 则检查和添加缺失的字段; 如果指定了force=true, 则会删除表重建.
例:
> addtable("item")
(删除表item并重建)
> addtable("item", true)
[TODO: reload]
重新加载metadata. 当修改了DESIGN.wiki中的表结构定义时, 应调用该命令刷新metadata, 以便showtable/addtable等命令使用最新的metadata.
[addcol]
addcol {table} {col}
添加字段{table}.{col}
[getver]
取表定义的version.
[getdbver]
取数据库的version.
[import]
参数: {filename} {noPrompt=false} [encoding=utf8]
将文件内容导入表,如果表不存在,会自动创建表(根据metadata),如果表已存在,会删除重建。文件编码默认为utf8. (TODO: 支持指定编码)
一个文件可以包含多个表,每张表的数据格式如下:
# table [CarBrand]
id name shortcut
110 奥迪 A
116 宝骏 B
103 宝马 B
...
"#"开头为注释,一般被忽略;特别地,"table [表名]"会标识开始一个新表,然后接下去一行是header定义,以tab分隔,再下面是数据定义,以tab分隔。
这种文件一般可以在excel中直接编辑(但注意:excel默认用本地编码,也支持unicode即ucs-2le编码,但不直接支持utf-8编码)
注意: - 如果字段值为空, 直接写null.
例:导入车型测试数据 先生成测试数据:
initdata\create_testdata.pl
(生成到文件brands.txt)
再在upgrade.php中用import导入:
> import("../initdata/brands.txt")
注意:如果列名以"-"开头,则忽略此列数据,如
# table [CarBrand]
id name -shortcut
110 奥迪 A
...
将不会导入shortcut列。
TODO: 带关联字段导入:
# table [Figure]
name bookId(Book.name) ref
黄帝 史记 本纪-五帝
上例数据中,表示根据Book.name查找Book.id,然后填入Figure.bookId。如果Book中找不到相应项,会自动添加一项。
关联表导入:
# table [Svc]
id name Svc_ItemType(ittId,svcId)
1 小保养 1,2,6
2 大保养 1,2,3,4,7
上例有个字段表述为"Svc_ItemType(ittId,svcId)", 它表示该字段关联到表 Svc_ItemType.ittId字段,而本表的id对应关联表字段svcId。其内容为以逗号分隔的一串值。以上描述相当于:
# table [Svc]
id name
1 小保养
2 大保养
# table [Svc_ItemType]
ittId svcId
1 1
2 1
6 1
...
还可以这样设置:
# table [Svc]
id name Svc_ItemType(ittId,svcId,ItemType.name)
1 小保养 机油;机油滤清器
上例中"Svc_ItemType"多了一个参数"ItemType.name", 它表示下面内容是关联到ItemType.name字段,即需要先用"SELECT id FROM ItemType WHERE name=?"查询出Svc_ItemType.ittId(第一个参数),再同上例进行添加。
更新指定表(addtable命令):
upgrade.php car_brand car_series car_model
更新所有表(initdb命令):
upgrade.php all
版本差量升级(TODO:upgrade命令):
upgrade.php upgrade
当表结构变化时,
upgrade.php
ver = getver();
dbver = getdbver();
if (ver <= dbver)
return;
if (dbver == 0) {
initdb();
return;
}
if (dbver < 1) {
addcol(table, col);
execsql('update table set col=col1+1');
}
if (dbver < 2) {
addtable(table);
importdata('data.txt');
}
if (dbver < 3) {
addkey(key);
}
if (dbver < 4) {
altercol(table, col);
}
update cinf set ver, update_tm
版本发布又称“部署”或“版本上线”,是将开发版本进行构建和优化后,上传线上服务器的过程。
筋斗云框架使用webcc组件进行版本发布,对构建后的版本,也要求git进行代码库管理。 (注:v5.2起,常常使用另一种更简单的方式,不使用webcc也不创建online版本库,直接使用源码上线,参考下一小节介绍)
webcc提供的功能主要有:
版本发布的配置包括:
发布或上线过程很简单,直接在git bash中运行 build_web.sh 即可。 (注意:它会用到curl, bash等工具,好在git工具包中已包含这些。)
[开发版本号与发布版本号]
在构建后,online文件夹中会自动生成文件revision.txt
,代表当前开发版本号。下次构建时通过检查该版本与最新开发版本间的差异,可实现差量构建(注意:其中还包含依赖文件管理)。
在上传服务器后,会自动在服务器上生成文件revision_rel.txt
, 代表当前发布版本号。下次上传时,只会进行差量上传。
使用webcc可以对应用进行较好的打包与发布(类似webpack,但可以更灵活的处理打包的逻辑页数),其缺点是:
源码部署指的是不使用webcc,直接将源码通过git push推送到服务器就实现上线,且可直接在线上修改或提交代码。
[创建线上代码库和部署web目录]
习惯上部署用户使用builder, web服务器用户(即apache用户)为www。注意以下操作中,应设置相关访问权限,实现两者创建的目录或文件相互可读或可写。 以builder用户在apache用户主目录下(一般是/var/www)创建src目录,使用git_clone.sh工具创建git代码库。 假设项目名为myproject:
mkdir /var/www/src
cd /var/www/src
git_clone.sh myproject
需要实现当推送新版本后,前端应可直接热更新(即自动重新刷新)。在使用webcc时,会自动生成源码版本文件revision.txt,框架会读该文件获取版本号实现热更新。 在使用源码部署后,应实现推送后创建和更新该文件。方法是在线上设置版本库:
cd /var/www/src/myproject
vi .git/hooks/post_update
找到下面一句打开注释:
git log -1 --format=%H > server/revision.txt
在web目录下(一般是/var/www/html)为应用创建链接:
cd /var/www/html
ln -sf /var/www/src/myproject/server ./myproject
这样就可以直接访问项目如:http://myserver/myproject/
。注意server目录才是线上目录,要用它来创建链接。
在builder主目录下创建链接,便于推送:
ln -sf /var/www/src ./
这样要上线,就只要使用git直接推送到地址: myserver:src/myproject
[缓存设置]
即设置apache的.htaccess文件。
使用webcc编译优化时,移动端的缓存策略是:
在不使用webcc时,应将m2/.htaccess中设置为对html/js/css均不允许缓存。 否则更新js/css后,前端(尤其是微信)无法得到更新。(如果出现此种情况,只能手工修改html中对css/js的引用链接,比如加参数)。
做法可以直接拷贝web/.htaccess到m2/.htaccess,注意修改后应提交代码库。
[移动端加载优化]
使用webcc时,会对html/js/css/小图标等做压缩优化,以加快移动端加载速度。
使用源码部署后,可酌情做部分优化。
示例:对index.html文件,将不常修改的css/js文件(比如第三方库、jdcloud框架库等)合并压缩。
先在html去掉要优化合并的js/css文件,改为放置:
<!--link rel="import" href="lib.html" /-->
<link rel="stylesheet" href="lib.min.css" />
<script src="lib.min.js"></script>
将要合并压缩的js/css列举在m2/Makefile中。运行make命令生成上面引用的三个文件(内部使用了tool/mergeJsCss.sh工具),别忘记将它们加到代码库。 其中lib.min.css和lib.min.js即是压缩后的代码。而lib.html用于直接调试合并前的js/css源码,使用时打开该行注释,并注释掉下面两行。
注意当任何被优化的js/css文件修改后,应重新运行make更新相关文件并提交代码库。
对于Web应用,每次浏览器打开时均已是最新版本;但是如果浏览器一直未关闭,则需要手工刷新页面才能更新。
在手机上,特别是将Web应用打包为手机原生应用后,当服务器升级后,用户必须将应用重新打开才能获得最新版本,这相当于在浏览器中刷新。 如果用户一直不退出应用(这在手机上很常见,应用会在后台一直缓存着),必须有机制能保证版本更新后可自动刷新。
筋斗云框架支持客户端自动升级,原理如下: - 服务端API自动将版本号通过HTTP头X-Daca-Server-Rev
发送给客户端。版本号通过全局变量API_VER
设定,或从文件revision.txt
读取(注意该文件由webcc发布时自动生成),版本号最多为6位。 - 客户端在访问API时,检查版本号是否与本地之前缓存的版本号一致,如果不一致则自动刷新回首页。
通过以上过程,用户不必退出应用再重新打开,就能实现版本自动升级。
本节介绍目录 server/tool/ 下的工具。
注意:server/tool/
目录下的工具随项目一起发布,一般通过网络访问。而tool/
目录下的工具一般是命令行工具,不发布。
log.php(f?=ext, sz?=2500)
查看日志(只显示最新的若干条,倒序排列)。
[参数]
[示例]
log.php
log.php?f=trace
(查看trace log)
init.php(ac?)
数据库初始化或配置文件初始化。可用的ac参数见源文件内部文档。
项目初始化方法参考前面章节"服务端部署与升级"->"初始化配置".
tool.php(ac?)
具体参数见源程序内部文档。
工具包。目前支持base64编码、解码,md5编码等。
tool/upgrade/index.php(diff?=0)
访问该链接,将根据server/tool/upgrade/META文件自动创建或升级数据库。
META文件可在tool目录下运行:
make meta
获得更新。其实质是通过upgrade.php工具解析设计文档(DESIGN.md)生成META文件。
php upgrade.php export
该工具已集成到tool/init.php中,用于线上自动升级数据库。
例如, 想每天执行一次某任务(假如名为task1
), 可以在server/tool/task.php
中添加一个名为ac_task1
的函数:
function ac_task1()
{
// 执行任务
}
注意: 与api_functions.php等在Web服务器环境下执行不一样, 它是作为命令行工具执行, 例如手工执行名为db
的任务:
php ./task.php task1
由于包含了api.php, 它也可以用queryOne/execOne等函数直接操作数据库, 或用callSvcInt等函数内部调用服务器接口. 注意由于它是未登录的环境, 若是调用有登录权限检查的接口会因未登录而出错, 以确保安全可靠的前提下, 可以通过以下方式调用:
$_SESSION["uid"] = 1; // 模拟id=1的用户登录.
$rv = callSvcInt("XX.query"); // 这样就可以调用AC1_XX类的接口.
当然也可以用system函数调用外部命令.
[部署]
通过Linux的crontab机制设置定期执行任务。推荐的部署方法是:
php task.crontab.php
生成一串文本,复制这些文本.crontab -e
编辑计划任务,将刚刚生成的文本复制过来即安装好。在计划任务执行时, 本来输出到屏幕的内容会输出到日志文件 tool/task.log
, 检查日志以查看任务执行情况.
具体设置参照task.crontab.php中的注释说明.
[数据库备份任务:db]
作为示例, task.php中自带一个db
任务, 在task.crontab.php中设置每天执行一次, 用于备份数据库。 备份脚本是backup_db.sh, 修改其中的数据库连接设置即可实现数据库日备份.
[需求]
考虑以下灾难场景及恢复方式:
[备份建议]
[目前方案]
[注意]
筋斗云框架从v3.0开始支持插件,即plugin下的各目录,因与框架项目在一起,称为内置插件(下一章节介绍); 到v5.5开始支持外部独立插件,便于独立维护,新的插件都应以这种方式创建和维护,本节只介绍新的插件机制。
工具tool/jdcloud-plugin.sh用于安装、卸载和创建插件。文件plugin.dat用于记录插件安装信息。
用法:须在项目目录下调用,假设项目目录为myproject, 插件为jdcloud-plugin-notify. 两个目录必须都使用git管理。
# 用git下载插件,注意与项目目录平级
git clone server-pc:src/jdcloud-plugin-notify
cd myproject
# 安装
./tool/jdcloud-plugin add ../jdcloud-plugin-notify
# 删除
./tool/jdcloud-plugin del jdcloud-plugin-notify
也可以用
./tool/jdcloud-plugin del ../jdcloud-plugin-notify
插件的目录结构与项目目录一致。
安装时,将插件目录下的文件复制到项目相应目录下,或对于项目中已有的文件,则会将内容合并到相应文件中,然后将安装信息写入plugin.dat文件中,并将相关文件均添加到git。 删除插件时,根据plugin.dat中的相应数据,删除文件或删除共享文件中插件的内容。 目前不支持直接更新插件,可先删除再安装。
特别地,插件下的README.md文件将被复制到项目下的路径server/plugin/{插件名}.README.md
。
plugin.dat格式如下:
{pluginName} #{git版本}
{pluginName} {newFile}
{pluginName} +{appendfile}
版本行只用于记录版本,无处理逻辑。标记为+的文件在删除插件时,会自动将追加的内容删除掉,而不是删除整个文件。
假如要创建名为jdcloud-plugin-url的插件,须先在plugin.dat中注册插件需要的所有文件,如:
jdcloud-plugin-url #init
jdcloud-plugin-url server/url.php
jdcloud-plugin-url +server/plugin/index.php
其中第1行表示插件名;第2行表示将指定文件添加到插件;第3行中,文件名前有加号,表示只需要该文件将特定标识内的内容添加到插件。
特定标识为{插件名} BEGIN
和{插件名} END
,其中的内容即为插件内容,特定标识一般包在注释内,示例:
/*! jdcloud-plugin-url BEGIN */
function api_getShareUrl()
{
...
}
/*! jdcloud-plugin-url END */
创建一个说明文件,路径为server/plugin/{插件名}.README.md
。这里创建server/plugin/jdcloud-plugin-url.README.md
。 该文件将对应插件目录下的README.md文件。
接下来,创建独立的插件目录和代码库,它与项目平级:
git init jdcloud-plugin-url
然后回到项目目录,调用命令,根据plugin.dat的内容创建插件到指定路径:
./tool/jdcloud-plugin create ../jdcloud-plugin-url
然后进入插件目录,提交文件到git库即可。
若要更新插件,也是相同步骤:更新plugin.dat,调用jdcloud-plugin create
命令,提交文件到插件git库。
支持版本: v3.0
plugin/ - 插件总目录
plugin/index.php - 插件配置文件,指定应用使用哪些插件
plugin/{pluginName}/ 某插件的主目录
plugin/{pluginName}/DESIGN.wiki 插件设计文档
plugin/{pluginName}/plugin.php 插件配置及服务端接口
plugin/{pluginName}/m2/page/{page}.[html|js] 插件前端逻辑页面,可直接在应用程序中使用
plugin/{pluginName}/m2/plugin.js 可选,插件前端全局逻辑,文件名字任意,在plugin.php中通过return语句指定使用。
TODO: plugin/{pluginName}/m2/plugin.css 可选,插件前端全局样式,文件名字任意,在plugin.php中通过return语句指定使用。
插件设计文档的结构可参考主设计文档,一般也包括 概要设计(需求),数据库设计,交互接口,前端应用接口这些部分。
插件设计文档中的表应在升级时自动创建,主设计文档中有以下指定用于此目的:
@include server/plugin/*/DESIGN.md
插件中对外部表及字段的依赖,应使用@see指令标注:
依赖数据接口:
@see @User: id, phone
@see @Store: id, name, dscr
@see @Ordr: id, storeId
Ordr.storeId = Store.id
这些被依赖的字段,看似名字固定,其实是可配置的,详参后端接口文档,查询 PluginBase.$colMap 关键字。
[插件配置文件 plugin/index.php]
该文件被后端应用框架自动包含,其内容示例如下:
<?php
Plugins::add([ "plugin1", "plugin2" ]);
表示当前应用使用两个插件"plugin1"和"plugin2", 分别对应目录 plugin/plugin1和plugin/plugin2. 如果该文件不存在,则后端应用不加载任何插件。
[插件后端 plugin/{pluginName}/plugin.php]
文件会被自动包含到应用api.php中。在这里实现插件的交互接口,在文件的最后可以返回插件的配置,如
<?php
// 实现交互接口 svcinfo
function api_svrinfo()
{ ... }
// 返回插件配置
return [
"js" => "m2/plugin.js" // 前端需要包含的文件
];
详参后端接口文档 模块api_fw -> 插件机制 章节。
[插件移动WEB前端 plugin/{pluginName}/m2/page/]
和应用的 m2/page/ 目录一样,包含插件的每个逻辑页面。
将插件直接复制到plugin目录下,在plugin/index.php中添加该插件名即可。
TODO: 安装相关API和交互接口
TODO: 相关表更新 用upgrade.php
Plugins.add($pluginList); // 添加插件
Plugins.exists($pluginName); // 判断插件是否存在
TODO: 服务端安装插件:
Plugins.install('plugin1@1.1'); -- 注册的插件, 下载到本地,解压到plugin目录下,再自动更新plugin/index.php文件。
Plugins.uninstall('plugin1'); -- 删除插件目录,再更新plugin/index.php。
MUI.initClient(); // 前端初始化,如需调用以下接口,须在muiInit事件中调用。
Plugins.exists(pluginName); // 判断插件是否存在
Plugins.list(); // 返回当前应用的插件列表
示例:
$(document).on("muiInit", myInit);
function myInit()
{
MUI.initClient(); // 初始化客户端环境,包括插件
...
}
// 判断和使用插件前端页面
if (Plugins.exists('plugin1')) {
MUI.showPage('#plugin1-page1');
}
返回插件列表:
initClient() -> { @plugins? }
plugins:: { name => {js?} }
TODO: 安装与卸载
addPlugin(name)
delPlugin(name)
应用专属插件可直接存放在plugin目录下,使用与主应用相同的代码库。
在上线时,不同的项目分别创建一个build_web.sh,根据配置不同选择不同的插件更新到服务器。 文件plugin/index.php不上线,必须手工上传服务器。
build_web.sh
export CFG_PLUGINS=plugin1,plugin2
tool/make_install.sh
通用插件使用专门的代码库维护版本。如果要加到工程中,也可以放到plugin目录下作为子模块加到应用代码库中。
注意: