梁健 - 最近更新于2015/12
概念:
注:以下标记*表示可变文件,根据实际应用添加内容。
[根目录]
DESIGN.wiki* -- 主设计文档。其它文档在doc目录下。 build_web.sh -- Web部署工具。
产品设计文档包括:
infrastructure; main app; incrementatal app;
[后端应用 - server目录]
内部实现部分:
[回归测试 - rtest目录]
内部实现部分:
[文档 - doc目录]
[工具 - tool目录]
[部署环境]
[开发环境]
[演示版]
演示版用于快速开发原型及测试演示,对运行环境低要求,部署极其简单。
[运行环境]
服务支持mysql和sqlite数据库,服务连接mysql数据库仅当
[测试模式 - TEST_MODE]
服务支持测试模式,它连接名为carsvc_test的数据库,并允许一些清除操作。仅当:
以上设置项意味着:
[配置选项]
此处列出用到的配置项, 具体用法请搜索本文档. server指服务端配置(包括upgrade.php工具), test指手工测试(如client.php), rtest指自动回归测试(run_rtest.pl).
php/conf.user.php
(for server), 该文件存在则自动加载,用于设置自定义配置选项,如定义数据库用户名密码: putenv("P_DBCRED=...")
[模拟模式 - MOCK_MODE]
当应用目录下存在文件 CFG_MOCK_MODE 时,服务运行于模拟模式。 或者,当应用处理测试模式下,且应用目录下有文件 CFG_MOCK_T_MODE, 服务运行于模拟模式。 这时,对外部系统的依赖(如短信模块,微信接口,支付宝支付等)都将模拟运行,只生成日志到 ext.log 中。
可通过工具 tool/log.php 查看日志。
在应用设计时,一般使用前缀名为"P_"的环境变量供使用时扩展。
根据不同的部署环境,可创建和配置文件php/conf.user.php
。一般使用putenv设置环境变量或用ini_set设置参数。
例如:
[数据库密码]
例:
putenv("P_DB=localhost/carsvc"); putenv("P_DBCRED=bGo6aWhxZ19VR0xH");
在线上,如果同时支持测试模式(TEST_MODE)和生产模式,则测试模式应使用专门的DB, 可以用变量P_DB_TEST设置:
putenv("P_DB=localhost/carsvc"); putenv("P_DBCRED=bGo6aWhxZ19VR0xH"); putenv("P_DB_TEST=localhost/carsvc_test"); // putenv("P_DBCRED_TEST=bGo6aWhxZ19VR0xH"); // 可设置登录用户及密码,不设置则使用P_DBCRED
[URL路径]
如部署路径为 http://oliveche.com/cheguanjia/ ,则设置
putenv("P_URL_PATH=/cheguanjia");
如果不设置,应用将自动判断(但如果服务器上使用了符号链接,则会判断失误)。
注意:
变量/函数名/数据库表的字段名使用驼峰式(首个单词小写,其余单词首字母大写),如getCarModel, svcId。
WebAPI的调用名和传入参数也采用驼峰式,由于目前实现时不区分大小写, 所以调用时也可以用全部小写(习惯上,url里常常只用小写字母,且不带下划线。) 例如,调用接口名queryOrder,传入参数为modelId, storeId等。
类名,或数据库表名,用大驼峰式(或叫Pascal命名,所有单词首字母大写), 如OrderStatus.
根据[系统建模]设计数据库表结构。
[通用规则]
以"Id"结尾 | Integer |
以"Price"/"Total"/"Qty"/"Amount"结尾 | Currency |
以"Tm"/"Dt"/"Time"结尾 | Datetime/Date/Time |
type=(n) | String |
type=& | Integer |
type=@ | Currency |
type=# | Double |
以"Flag"结尾 | TinyInt(1B) NOT NULL |
此外, 如果无明确标识, 缺省为字符串类型. 例如,"total", "docTotal", "total2", "docTotal2"都被认为是Currency类型。
name(s)
,字串长度以如下方式描述:
s | small=20 |
m | medium=50 (default) |
l | long=255 |
t | text |
注意:
TODO: define unique-key, index, not null, default value
客户端通过HTTP协议与服务端交互,调用服务端WebAPI。
GET /api.php?ac=fn&p1=value1&p2=value2
,
也可以用POST请求实现:
POST /api.php?ac=fn Content-Type: application/x-www-form-urlencoded p2=value2&p1=value1参数"ac"或"_ac"标识方法名, 必须使用URL参数传递, 其它参数未加说明的, 可以选择通过URL或POST传参.
POST /api.php?ac=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返回类型所定义。
以下面的WebAPI描述为例:
根据id取车型信息: getModel(id) -> {id, name, dscr}
它包含以下信息:
GET /api.php?ac=getModel&id=100
URL参数ac
表示WebAPI名称,一般用全小写。
为防止服务端缓冲,一般请求时还应加上一个随机参数,如 GET /api.php?ac=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 => "禁止操作" ];
TODO: 审查每项错误信息.
[关于空值]
假如有参数"a=1&b=&c=hello", 其中参数"b"值为空串。 一般情况下,被当作未赋值处理,即与"a=1&c=hello"意义相同。
只有在对象保存上下文中(典型的是通用表操作的{tblname}.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="经度/Double,纬度/Double".
"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
.
这种格式一般用于server/cient间传递简单的表。
json.tbl(id, name) 或 json({h:["id","name"],d:[row1,row2...]})
适合传递普通的表。
php([{id,name}]) 或 php.objarr(id, name)
适合存储复杂数据到一个简单字段,但不用于server/client间数据交互。 它使用php的serialize/unserialize生成,格式如:
a:2:{i:0;a:2:{s:2:"id";i:10;s:4:"name";s:5:"liang";}i:1;a:2:{s:2:"id";i:11;s:4:"name";s:4:"wang";}}
上述字段在server/client交互时应使用等价的json.objarr(id,name) 如
[ {id: 10, name: "liang"}, {id: 11, name: "wang"}, ]
或 list(id,name) 如
@id:name,10:liang,11:wang
适合客户端向服务端通过http content传大表。如 csv(id,name,dscr)
如
urlist(id, qty)
可表示
items[0][id]=101&item[0][qty]=2&items[1][id]=102&item[1][qty]=4
如果一个查询支持分页(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协议进行通信。
注意:
# for https, ignore cert errors (e.g. for self-signed cert) curl_setopt($h, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($h, CURLOPT_SSL_VERIFYHOST, 0);
特殊的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可用于指定所属应用, 如不指定默认值为"carsvc". 它隐含着session的名称为"{_app}id"如请求
GET /api.php?_app=emp
第一次访问将返回
SetCookie: empid=xxxxxx
URL参数"_test"值为非0时表示测试模式。参考章节"概要设计"->"配置项"->"测试模式".
当_test值为2时,表示运行回归测试。
通过在index.php中解析$_SERVER["PATH_INFO"]
, 可以令URL更可读:
http://localhost/cheguanjia/api.php?ac=login&phone=137&pwd=1234 -> http://localhost/cheguanjia/api.php/login?phone=137&pwd=1234
对于对象的CRUD,URL像这样:
http://localhost/cheguanjia/api.php?ac=Ordr.query&res=id,dscr -> http://localhost/cheguanjia/api.php/Ordr/query?res=id,dscr
即"Ordr/query"转化为"ac=Ordr.query".
URL参数"_ver"值为客户端版本。取值参考表ApiLog.ver字段。目前只有安卓客户端设置该参数为"a/{ver}" (如"a/2"), 其它客户端版本根据userAgent自动获取。
要访问每个API,必须拥有相应的权限。权限定义如下:
员工登录后可能获得以下一个或多个权限(由Employee.perms指定):
其它权限:
如果API未明确指定权限,则认为是AUTH_GUEST.
按登录对象的不同, 权限又分为 用户类(AUTH_USER) 和 商户类(AUTH_STORE), 分别对应 AC1_xxx 和 AC2_xxx 系列原子权限. 这也使得同一个API在不同权限下的执行结果可能不同,如 Ordr.query
在AUTH_USER下(查该用户订单)或AUTH_STORE下(查该商户订单)是不同的。
一个用户可以拥有多个权限, 但按data ownership范围的不同, 只会有一个ownership level. 定义如下:
execSql(sql, wantArray?, wantId?, fmt?) -> rowSet or affectedRows or lastInsertedId
只有管理员登录后才能使用, 或是测试模式下才能使用. 生产模式下客户端和商户端禁止使用.
如果是SELECT语句, 返回结果集, 否则返回affectedRows.
[权限]
[参数]
[示例]
请求
execSql(sql="SELECT COUNT(*) AS N FROM User")
返回
[ {N: 3} ]
注:
[示例]
请求
execSql(sql="SELECT COUNT(*) AS N FROM User", fmt=one)
返回
3
[示例]
请求
execSql(sql="SELECT COUNT(*) AS N, MIN(createTm) AS createTm FROM User", fmt=one)
返回
[3, '2015-1-1']
[示例]
请求
execSql(sql="SELECT COUNT(*) AS N FROM User", wantArray=1) 或 execSql(sql="SELECT COUNT(*) AS N FROM User", fmt=array)
返回
[ [3] ]
[示例]
请求
execSql(sql="SELECT id FROM User")
返回
[ {id: 1}, {id: 2}, {id: 3} ]
[示例]
请求
execSql(sql="SELECT id FROM User", fmt=array)
返回
[ [1], [2], [3] ]
[示例]
请求
execSql(sql="SELECT id FROM User", fmt=table)
返回
{ h: ["id"], d: [ [1], [2], [3] ] }
[示例]
请求
execsql(sql="DELETE User WHERE id=1 or id=2")
返回
2
注: 对于非SELECT语句, 返回affectedRows
以下4条API完成对表的增删改查(CRUD)动作. 服务端可根据当前用户所拥有权限进行检查.
{tblname}.add(res?)(POST fields...) -> {id} {tblname}.set(id)(POST fields...) {tblname}.get(id, res?) -> {fields...} {tblname}.del(id) {tblname}.query(res?, cond?, @res2?, join?, @cond2?, union?, distinct?=0, _pagesz?=20, _pagekey?, _fmt?) -> tbl(fields...) 取对象数组: {tblname}.query(wantArray=1, @subobj?, res?, ...) -> [obj1, obj2...] 分组统计: {tblname}.query(gres, res, cond) -> tbl(fields...)
缺省这些操作只对管理员登录后开放, 用户或商家登录后无权操作. 管理员可对所有表的所有字段进行CRUD操作.
部分对象对用户或商家登录后开放, 如用户登录后可操作User,UserCar等对象, 这些操作会在相应章节单独列出来说明, 如"用户车辆信息"章节. 在这些章节中会对操作限制(如只能get/set,不能add/del), 只读字段(即使请求中给出,服务端也会忽略这些字段), 可缺省字段(服务端会自动补全)做出说明.
系统中的表设计(表名, 字段等)参见"数据库设计"章节. 一般表都设计为使用整形字段id作为主键. 该字段创建后变只读, 不允许被修改.
对于add操作,默认返回{id}, 如果想多返回其它字段,可设置res,如
Ordr.add(res="id,status,total")(POST fields...) -> {id, status, total}
[query]
query可以用参数cond指定查询条件, 如
cond="type='A' and name like '%hello%'" URL编码后为 cond=type%3d%27A%27+and+name+like+%27%25hello%25%27
query返回有两种形式, 缺省返回table类型便于支持分页, 但不支持查询子对象(subobj参数). 如果指定wantArray=1, 可以返回子对象, 但则不支持分页. 例如, query缺省返回:
{ "h": ["id", "name"], "d": [[1, "liang"], [2, "wang"]] }
如果指定wantArray=1则返回:
{ [["id": 1, "name": "liang"], ["id": 2, "name": "wang"]] }
注意:
[分页]
注意:
详细请参考章节"分页机制".
[参数]
$_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 ], // 已评价的订单 ] ]
[操作特殊属性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属性:
set flags=concat(ifnull(flags, ''), 'f')
set flags=replace(flags, 'f', '')
注意:
[例: 添加商户]
添加商户, 指定一些字段:
Store.add() name=华莹汽车(张江店) addr=金科路88号 tel=021-12345678
注:
POST /api.php?ac=Store.add Content-Type: application/x-www-form-urlencoded name=华莹汽车(张江店)&addr=金科路88号&tel=021-12345678
操作成功时返回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 '%浦东%'
注意: 内容经UTF8+URL编码, 测试时大致是这个样子:
测试: client.php callsvr store.query null "res=id,name,addr&cond=name like '%25%E5%8D%8E%E8%83%9C%25' and addr like '%25%E6%B5%A6%E4%B8%9C%25'"
或
client.php callsvr store.query null "['res'=>'id,name,addr', 'cond'=>'name like \'%华胜%\' and addr like \'%浦东%\'']"
(必须在UTF8 shell中运行, 否则编码不对)
操作成功时返回内容如下:
{ "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)
操作成功时无返回内容.
使用multipart/form-data格式上传(标准html支持,可一次传多个文件): upload(type?=default, genThumb?=0, autoResize?=1)(POST content:multipart/form-data) -> [{id, thumbId?}] 直接传文件内容,一次只能传一个文件: upload(fmt=raw|raw_b64, f, exif?, ...)(POST content:raw) -> [{id, thumbId?}] TODO: 如果使用微信的上传接口, 可以调用: upload(..., weixinServerIds) -> [{id, thumbId?}]
上传照片等内容. 返回附件id. 因为允许一次上传多个文件,返回的是一个数组,每项对应上传的一个文件。
员工端上传图片时应传图片扩展信息(exif信息),包括时间、GPS信息,以便后期(通过工具校验数据)验证员工是否在指定的时间地点去洗车。 注意:exif信息只在上传单图时有意义。
TODO:将限制只支持jpg等几种指定格式; 以及限制最大可传文件的size
[参数]
{"DateTime": "2015:10:08 11:03:02", "GPSLongtitude": [121,42,7.19], "GPSLatitude": [31,14,45.8]}
type决定生成缩略图的大小:
[返回]
thumbId: 如果参数设置了genThumb=1, 则会生成缩略图并返回该字段为生成的缩略图id.
[示例1]
使用Content-Type为multipart/form-data可一次上传一个或多个文件。 假如上传两个文件"file1.txt"和"file2.txt", HTTP请求如下:
POST api.php?ac=upload Content-Type:multipart/form-data; boundary=----WebKitFormBoundary6oVKiDmuQSPOtt2L Content-Length: ... ------WebKitFormBoundary6oVKiDmuQSPOtt2L Content-Disposition: form-data; name="file1"; filename="file1.txt" Content-Type: text/plain (content of file1) ------WebKitFormBoundary6oVKiDmuQSPOtt2L Content-Disposition: form-data; name="file2"; filename="file2.txt" Content-Type: text/plain (content of file2) ------WebKitFormBoundary6oVKiDmuQSPOtt2L--
注意:
使用html自带的form和file组件可以自动生成这样的POST请求,如上传两个文件:
<form action="api.php?ac=upload" method=post enctype="multipart/form-data"> <input type=file name="file1" accept="image/*"> <input type=file name="file2" accept="image/*"> <input type=submit value="上传"> </form>
也可以只使用一个file组件,只要设置属性multiple以允许多选文件(以chrome为例,在文件选择框中可以按Ctrl键多选文件),但注意此时name必须带中括号:
<form action="api.php?ac=upload" method=post enctype="multipart/form-data"> <input type=file name="file[]" multiple="multiple" accept="image/*"> <input type=submit value="上传"> </form>
multiple属性是html5新增属性,如果浏览器不支持,也可以这样写两个文件上传框:
<form action="api.php?ac=upload" method=post enctype="multipart/form-data"> <input type=file name="file[]" multiple="multiple" accept="image/*"> <input type=file name="file[]" multiple="multiple" accept="image/*"> <input type=submit value="上传"> </form>
返回示例如下:
[ {id:1, thumbId:2}, {id:3, thumbId:4} ]
[示例2]
使用fmt=raw上传:
POST api.php?ac=upload&fmt=raw&f=1.jpg Content-Type: image/jpeg Content-Length: ... (jpg文件内容)
使用fmt=raw_b64上传:
POST api.php?ac=upload&fmt=raw_b64&f=1.jpg Content-Type: text/plain Content-Length: ... /9j/4AAQSkZJRgABAQEASABIAAD/4QC+RXhpZgAATU0AKgAAAAgABgE...
返回示例:
[ {id:1, thumbId:2} ]
att(id) 根据id下载附件. att(thumbId) 使用缩略图id获取原图.
注意: 该调用不符合接口规范, 它不返回格式为"[code, data]"的json串, 而是直接返回文件内容.
HTTP header "Content-Type"将标识正确的文件MIME类型,如jpg类型为"image/jpeg".
如果找不到附件,将返回HTTP状态码"404 Not Found"。
[参数]
[示例]
获取id=100的附件:
GET api.php?ac=att&id=100
已知缩略图id=100, 获取它对应的原图:
GET api.php?ac=att&thumbId=100
返回
找到图片:
HTTP/1.1 200 OK Content-Type: image/jpeg (图片内容)
一般浏览器可以正确直接显示该图片。
或找不到图片:
HTTP/1.1 404 Not Found Content-Type: text/plain; charset=UTF-8
upload_raw.php(f, d?=upload)(rawFileContent)
[参数]
可用于管理员上传文件。例如使用wget工具:
set PROG=wget -nv --user=lj --password=liang123 -t 1 --connect-timeout=6 %PROG% --post-file=d:/test/1.jpg "http://localhost/upload_raw.php?f=1.jpg&d=upload/test"
TODO: 验证管理员
浏览: upload_list.php(d?=upload)
文本模式浏览: upload_list.php(d?=upload, ac=list, f)
删除文件: upload_list.php(d?=upload, ac=del, f[])
可用于手工浏览服务器上的文件。方便下载或删除文件.
注意:权限控制:只能浏览该工具所在目录及其子目录。
TODO: 验证管理员
[参数]
[示例]
(list folder "upload"; admin user/pwd=liang/liang123) > curl "http://localhost:8080/upload_list.php?ac=list" -u liang:liang123 (list folder ".", filter file name by ".jpg") > curl "http://localhost:8080/upload_list.php?ac=list&d=.&f=.jpg" -u liang:liang123
genSign(_pwd, ...) -> sign
根据密码对所有待签名字段进行签名。一般地,非下划线开头的字段都是待签名字段(例如_pwd, _sign这些都不参与签名),特别的会专门说明。
该API一般仅用于测试。页面 partner/voucher.html
使用了这个API。
如果希望一个参数不参与签名,则设计它的名字以"_"开头,如"_ac".
sendSms(phone, content, channel?=0)
[权限]
[参数]
对一个或多个手机群发短信。
测试页面:tool/sms.html
SmsLog.query([cond], [_pagesz=20])
查询短消息发送记录. 默认按id倒序排列。
[权限]
[参数]
[请求示例] 查询手机号为123456678901的最近2条消息记录, 按照id降序排列
SmsLog.query(cond="phone=12345678901", _pagesz=2)
[返回示例]
{ "h": ["id", "phone", "content", "tm", "retval"], "d": [ [18, "12345678901", "验证码123872,请在5分钟内使用。", "2015-09-20 22:21:16", 0], [15, "12345678901", "验证码655288,请在5分钟内使用。", "2015-09-18 12:46:32", 0], ] ] }} ==== 给用户发消息 ==== {{{ notifyUser(orderId, content, noWeixin?)
给订单用户发消息,默认发微信和短信。
[权限]
[参数]
proxy(url)
代理访问url。
所有对API的调用(请求与响应)均记录到表ApiLog中供分析。
对一个session, 监控其API调用是否有异常,避免自动化操作行为,这时将返回 E_FORBIDDEN 错误。安全类异常将记录到日志文件 secure.log
注意:
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的返回结果 ]]
定义前端应用入口及调用参数。
每一个应用均应定义一个唯一的应用标识(app),如"emp", "emp-store"等。在调用交互接口时,框架会自动将应用标识作为参数传给后端。 应用标识中字符"-"之前的部分称为应用类型(app type),如果应用标识里没有"-",则应用类型与应用标识相同。应用类型常用于登录类型与权限控制。
例如,定义客户端应用标识app=user,其应用类型也是"user",在login交互接口中,对应用类型"user"将作用户登录处理(如查询用户表),登录成功后赋予其用户权限。 定义员工端应用标识为app=emp-store,它的应用类型是"emp",在后端将作员工登录处理(比如查询的是员工表),登录成功后赋予其员工权限。 定义管理端应用app=emp-adm,它与应用emp-store是相同类型,因而登录方式和权限是相同的,即应使用员工信息登录。
可见,不同的应用可以是相同的应用类型。在实现交互接口时,不同的应用标识也会使用不同的cookie名称,以避免多个应用同时使用时相互干扰。
筋斗云的移动应用可做为Web应用在浏览器中运行, 也可以接入微信公众号或支付宝服务窗, 也可以通过cordova框架包装在应用容器中提供android/ios应用程序.
移动应用按惯例放在m2目录下。以常用的客户端、员工端两个移动应用为例,其相关文件有:
客户端:
商户端:
此外还有文件:
移动应用的对外接口包括页面URL,允许的入口页面(entry),URL参数等。下面举例描述这些接口。
m2/index.html
这表示打开这个URL,就进入移动客户端应用。
该应用的应用标识定义为"user",表示用户登录。
[进入移动客户端并显示指定订单]
m2/index.html#order(orderId)
这表示可以请求这样的URL:
m2/index.html?orderId=32#order
其中: m2/index.html是页面地址, "?"后为参数(使用URL编码方式), "#"后为入口点, 表示允许进入的逻辑页面.
上例中的访问表示: 打开移动客户端的订单页面, 参数为orderId=32, 即显示32号订单.
m2/store.html
该应用的应用标识定义为"emp-store",表示员工登录(应用类型为emp)。
桌面应用按惯例放在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<>''
>=100 and <200
, null or 0 or 1
,不支持用括号组合条件。
如果同时对多个字段填写了搜索值,则表示这些条件需要同时满足,即AND
关系。
详细可参考文档 API参考 -> 筋斗云前端(桌面Web版) -> WUI.getQueryCond
移动应用和桌面应用的框架支持以下通用参数:
以下参数适用于移动应用:
注意: 一旦设置为非0, 则该值会被记住, 下次打开时即使未指定也会有值, 必须重新设置cordova=0清除(或在控制台中调用delStorage("cordova")).
以下参数适用于桌面应用:
移动应用和桌面应用使用以下JS全局变量:
以上变量可通过控制台手工调节部分参数.
[测试需求]
所有测试内容存放在rtest
目录下。
注:
> set SVC_URL=http://115.29.199.210/cheguanjia > 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
通过设置环境变量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).
> P_APP
指定app名称(间接指定session名). 对应系统URL参数"_app" (参考章节"应用标识(_app)). 在多种客户端同时登录时用于区分每个会话.
> P_SHARE_COOKIE
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
[实现]
rtest实现参见rtest/rtest.php
,服务端实现请在server/api.php
中搜索"CARSVC_TEST".
[原则]
[实现]
参考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
getLastLog(type)
[权限]
[参数]
我们使用的权限控制模型为: "用户-权限(即角色/原子权限组)-原子权限(基本权限)". (注:权限组也称"角色", 所以某些系统中也称为"用户-角色-权限"模型.)
在我们系统中, 用户主要有User和Employee两类.
权限有两类: authorization(或称permission), 以及data ownership.
在后台系统中, 对象操作类的原子权限定义是通过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中的[数据库设计]章节内容生成table meta data (@table)及meta version (@ver), 比照真正数据库中字段的cinf.version, 然后根据差异更新表设计及表内容.
注意:主设计文档中可以包含其它设计文档,指令如下: (支持版本: v3.1)
@include sub/mydesign.wiki
如果使用了插件,一般应包含所有插件文档,以便插件中的表也可被创建。在DESIGN.wiki中目前默认就有这样一行:
@include server/plugin/*/DESIGN.wiki
[数据库连接]
缺省地, upgrade.php与api.php连接相同的数据库. 支持mysql和sqlite. 环境变量P_DB可为升级工具指定数据库. 如
环境变量P_DBCRED指定连接数据库的用户名密码。如未指定,则使用php/dbcred.php中的设定。
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")
[upgrade]
自动根据版本差异升级数据库. 如果字段cinf.ver不存在, 则重建DB(但会忽略已有的表, 不会删除它再重新创建). 升级完成后设置cinf.ver字段. TODO: WebAPI "upgrade"可做同样的事情, 以便于通过web请求远程升级.
注意: 对于MYSQL数据库, 升级工具只创建表, 不创建数据库本身(以及权限设置).
[showtable]
参数: {table?="*"}
查看某表的metadata以及SQL创建语句. 参数{table}中可以包含通配符。
例:
> showtable("item") > showtable("*log")
[addtable]
参数: {table} [force=false]
根据metadata添加指定的表{table}. 未指定force参数时, 如果表已存在且未指定force=true, 则检查和添加缺失的字段; 如果指定了force=true, 则会删除表重建.
例:
> addtable("item") (删除表item并重建) > addtable("item", true)
[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")
注:
> select * from item limit 10 > update item set price=10 where id=3
[quit]
退出交互. 可简写为"q".
例:
> quit 或 > q
[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编码)
注意:
例:导入车型测试数据 先生成测试数据:
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(第一个参数),再同上例进行添加。
例:
upgrade.php car_brand car_series car_model upgrade.php all 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进行代码库管理。 webcc提供的功能主要有:
版本发布的配置包括:
发布或上线过程很简单,直接在git bash中运行 build_web.sh 即可。 (注意:它会用到curl, bash等工具,好在git工具包中已包含这些。)
[开发版本号与发布版本号]
在构建后,online文件夹中会自动生成文件revision.txt
,代表当前开发版本号。下次构建时通过检查该版本与最新开发版本间的差异,可实现差量构建(注意:其中还包含依赖文件管理)。
在上传服务器后,会自动在服务器上生成文件revision_rel.txt
, 代表当前发布版本号。下次上传时,只会进行差量上传。
对于Web应用,每次浏览器打开时均已是最新版本;但是如果浏览器一直未关闭,则需要手工刷新页面才能更新。
在手机上,特别是将Web应用打包为手机原生应用后,当服务器升级后,用户必须将应用重新打开才能获得最新版本,这相当于在浏览器中刷新。 如果用户一直不退出应用(这在手机上很常见,应用会在后台一直缓存着),必须有机制能保证版本更新后可自动刷新。
筋斗云框架支持客户端自动升级,原理如下:
X-Daca-Server-Rev
发送给客户端。版本号通过全局变量API_VER
设定,或从文件revision.txt
读取(注意该文件由webcc发布时自动生成),版本号最多为6位。
通过以上过程,用户不必退出应用再重新打开,就能实现版本自动升级。
本节介绍目录 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/task.php(ac)
它作为命令行工具执行。通过crontab设置定期执行该命令。
[安装]
进入tool目录执行php task.crontab.php
生成一串文本,再运行crontab -e
编辑计划任务,将刚刚生成的文本复制过来即安装好。
屏幕输出到日志文件 tool/task.log
.
[参数]
[ac=voucher]
建议每天执行一两次。 如果优惠券已过期,设置状态Voucher.status。发现优惠券的过期日期在3天内,发短信提醒用户。
[ac=order]
建议每小时执行一次。 隔天的订单,提前2小时通知用户。
[ac=db]
每天执行一次。 备份数据库。
[需求]
考虑以下灾难场景及恢复方式:
[备份建议]
[目前方案]
[注意]
支持版本: 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.wiki
插件中对外部表及字段的依赖,应使用@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目录下作为子模块加到应用代码库中。
注意: