通用Web产品后端框架设计

梁健 - 最近更新于2015/12

1 需求

2 概要设计

2.1 文件结构

概念:

注:以下标记*表示可变文件,根据实际应用添加内容。

[根目录]


DESIGN.wiki* -- 主设计文档。其它文档在doc目录下。

build_web.sh -- Web部署工具。

产品设计文档包括:

infrastructure; main app; incrementatal app;

[后端应用 - server目录]

api.php*
API接口应用。提供基于HTTP的访问接口,形式如:api.php/fn 或 api.php/obj.query;该文件包含其它实现文件,以及应用内共享的数据。其它应用可包含它从而直接以内部调用方式访问API接口。
app.php*
应用共享库。存放被多个项目所使用的数据。所有应用一般都应包含它。它包含common.php,app_fw.php,conf.php,conf.user.php等。
conf.php*
适用于所有应用(被app.php包含),保存易变逻辑。

内部实现部分:

php/common.php
通用共享库。基础公共函数部分,可适用一切php项目。
php/app_fw.php
应用框架库。为所有应用提供框架支持(以app_开头表示适用于所有应用,fw表示framework),被app.php包含。
php/conf.user.php*
可缺省,用于根据部署环境修改应用配置。被app.php包含。
php/api_fw.php
API接口应用的框架实现(以api_开头表示属于API接口应用)。被api.php包含。
php/api_functions.php*
API接口应用中的函数实现部分。被api.php包含。
php/api_objects.php*
API接口应用中的对象访问实现部分。被api.php包含。

[回归测试 - rtest目录]

rtest.php*
回归测试内容。(TODO: 类似api应用,可拆分为 rtest.php, php/rtest_fw.php, php/rtest_group1.php, php/rtest_group.php等)
run_rtest.pl
回归测试执行工具。
client.php
手工测试工具。模拟前端调用API接口。

内部实现部分:

WebAPI.php
测试应用框架库。

[文档 - doc目录]

index.html*
文档目录。修改它以增加文档。
__README.html
介绍文档管理。
后台框架.wiki
即本文,介绍通用产品框架设计。
Web应用部署
部署工具文档。
编码规范.wiki

[工具 - tool目录]

upgrade.php
数据部署工具。创建或更新数据库表,导入数据等。
webcc.php
Web部署工具。用户上传或更新线上Web产品目录。
cmdtool.template.php
使用app.php创建数据操作工具的示例,如用于特殊数据导入等。

2.2 运行环境

[部署环境]

[开发环境]

[演示版]

演示版用于快速开发原型及测试演示,对运行环境低要求,部署极其简单。

2.3 配置项

[运行环境]

服务支持mysql和sqlite数据库,服务连接mysql数据库仅当

[测试模式 - TEST_MODE]

服务支持测试模式,它连接名为carsvc_test的数据库,并允许一些清除操作。仅当:

以上设置项意味着:

[配置选项]

此处列出用到的配置项, 具体用法请搜索本文档. server指服务端配置(包括upgrade.php工具), test指手工测试(如client.php), rtest指自动回归测试(run_rtest.pl).

[模拟模式 - MOCK_MODE]

当应用目录下存在文件 CFG_MOCK_MODE 时,服务运行于模拟模式。 或者,当应用处理测试模式下,且应用目录下有文件 CFG_MOCK_T_MODE, 服务运行于模拟模式。 这时,对外部系统的依赖(如短信模块,微信接口,支付宝支付等)都将模拟运行,只生成日志到 ext.log 中。

可通过工具 tool/log.php 查看日志。

2.3.1 部署配置

在应用设计时,一般使用前缀名为"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");

如果不设置,应用将自动判断(但如果服务器上使用了符号链接,则会判断失误)。

注意:

2.4 命名规范

变量/函数名/数据库表的字段名使用驼峰式(首个单词小写,其余单词首字母大写),如getCarModel, svcId。

WebAPI的调用名和传入参数也采用驼峰式,由于目前实现时不区分大小写, 所以调用时也可以用全部小写(习惯上,url里常常只用小写字母,且不带下划线。) 例如,调用接口名queryOrder,传入参数为modelId, storeId等。

类名,或数据库表名,用大驼峰式(或叫Pascal命名,所有单词首字母大写), 如OrderStatus.

3 数据库设计

根据[系统建模]设计数据库表结构。

[通用规则]

以"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类型。

s small=20
m medium=50 (default)
l long=255
t text

注意:

TODO: define unique-key, index, not null, default value

4 通讯协议设计

4.1 通用原则

客户端通过HTTP协议与服务端交互,调用服务端WebAPI。

以下面的WebAPI描述为例:

根据id取车型信息:
getModel(id) -> {id, name, dscr}

它包含以下信息:

[错误码定义]

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"语义相同), 注意:不是置空字符串。

4.1.1 常用返回类型描述

{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

它表示:

[复杂类型序列化为字符串描述]

有时用一个字符串字段表示复杂的结构,这时常以下类型描述方式:

可表示 121.233543,31.345457

Coord="经度/Double,纬度/Double".

每个元组用","分隔, 元组内每个字段用":"分隔。每个字段内不能有这两个特殊符号(如果是日期,中间不可以有":", 如"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

4.1.2 分页机制

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

Ordr.query(_pagekey?, _pagesz?=20) -> {nextkey, total?, @h, @d}
或
Ordr.query(page, rows?=20) -> {nextkey, total, @h, @d}

[参数]

_pagesz
Integer. 页大小,默认为20条数据。
_pagekey
String (目前是数值). 一般某次查询不填写(如需要返回总记录数即total字段,则应填写为0),而下次查询时应根据上次调用时返回数据的"nextkey"字段来填写。
page/rows
为支持jquery-easyui而设置, 与_pagekey/_pagesz类似, 区别在于: 每次均返回total字段; 强制采用"limit"算法(默认如果没有用非主键排序,会采用"部分查询"算法), 意味着nextkey即下一页页码.

[返回值]

nextkey
String. 一个字符串, 供取下一页时填写参数"_pagekey". 如果不存在该字段,则说明已经是最后一批数据。
total
Integer. 返回总记录数,仅当_pagekey指定为0时返回。
h/d
实际数据表的头信息(header)和数据行(data),符合table对象的格式,参考上一章节tbl(id,name)介绍。

[示例]

第一次查询

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"属性,表示所有数据获取完毕。

4.1.3 分页机制实现原理

分页有两种实现方式:分段查询和传统分页。

分段查询性能高,更精确,不会丢失数据。但它仅适用于未指定排序字段(无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}

4.1.4 HTTPS服务

服务端支持HTTPS服务。客户端默认应使用HTTP协议与服务器通信;对于个别敏感的API,如涉及用户密码的登录、注册、修改密码等操作,应使用HTTPS协议进行通信。

注意:

4.1.5 调试等级(_debug)

特殊的URL参数"_debug"定义调试等级, 默认为0. 如果为1-9的数字, 将添加调试信息到结果数组中.

_debug=9: 输出SQL

通过设置环境变量P_DEBUG, 可以让本系统使用的测试工具(如client.php及rtest.php)请求时指定调试等级. 如:

set P_DEBUG=9
client.php callsvr usercar.query

4.1.6 应用标识(_app)

特殊的URL参数"_app"用于定义当前应用. 缺省值为"user"(对应客户端应用). 注意: 每个请求都必须带此标识, 它决定session对应的cookie项的名字. 如果不加该参数, 则可能出现未预料的错误.

当前可用应用名称定义如下:

user
缺省值. web移动应用, 用户使用的客户端.
admin
web桌面应用, 管理端.
store
web桌面应用, 商户管理端.
emp
web移动应用, 商户雇员端.

由于不同应用(如客户端, 商户端与管理端)共用一个api.php页面, 当在同一浏览器中打开多个应用时可能相互影响, 比如商户端与管理端同时使用时, 商户端会自动以管理端的权限操作.

解决方法是, 为不同应用使用不同的sessionId以区分. 实现时有两种方式, 一是访问不同的服务端页面, 如商户端访问api.php, 管理端访问api0.php(其中指定一个不同的sessionId); 另一种是商户端所有的请求都带一个参数指定sessionId. 我们将采用后一种解决方法.

URL参数_app可用于指定所属应用, 如不指定默认值为"carsvc". 它隐含着session的名称为"{_app}id"如请求

GET /api.php?_app=emp

第一次访问将返回

SetCookie: empid=xxxxxx

4.1.7 测试模式(_test)

URL参数"_test"值为非0时表示测试模式。参考章节"概要设计"->"配置项"->"测试模式".

当_test值为2时,表示运行回归测试。

4.1.8 使用PATH_INFO模式的URL

通过在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".

4.1.9 客户端版本(_ver)

URL参数"_ver"值为客户端版本。取值参考表ApiLog.ver字段。目前只有安卓客户端设置该参数为"a/{ver}" (如"a/2"), 其它客户端版本根据userAgent自动获取。

4.1.10 权限说明

要访问每个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. 定义如下:

4.2 通用操作

4.2.1 执行sql语句

execSql(sql, wantArray?, wantId?, fmt?) -> rowSet or affectedRows or lastInsertedId

只有管理员登录后才能使用, 或是测试模式下才能使用. 生产模式下客户端和商户端禁止使用.

如果是SELECT语句, 返回结果集, 否则返回affectedRows.

[权限]

[参数]

sql
String. SQL语句。
wantArray
Boolean. 如果非空,则对select语句的结果返回数组而非关联表. 由fmt=array替代, 已不建议使用.
wantId
Boolean. 如果非空,则对insert语句的结果返回最后插入的id而非记录数。
fmt
String. 指定select查询的结果返回格式: "table"-table格式({h,d}), "array"-array格式 (相当于wantArray=1), "one"-如果查询有多列,则只取首行, 如果查询只有一列, 则只取首行首列数据(相当于框架中的queryOne函数), 缺省: object aray / rowset

[示例]

请求

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.2.2 通用表操作

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

注意:

[分页]

_pagesz
Integer. 指定页大小, 默认一次返回20条数据。
_pagekey
String. 指定从哪条数据开始,应根据上次调用时返回数据的"nextkey"字段来填写。

注意:

详细请参考章节"分页机制".

[参数]

fields
每个字段及其值.
res, cond (get/query方法), orderby(query方法)
String. 指定返回字段及查询条件, 例如, res="field1,field2", cond="field1>100 AND field2='hello'", orderby="id desc", 注意使用UTF8+URL编码, 目前格式参照SQL语法, 字符串值应加上单引号.
distinct
Boolean. 如果为1, 生成"SELECT DISTINCT ..."查询.
res2, join, cond2 (get/query方法)
这几个字段只由内部使用,没有安全限制. res2, cond2为额外的字段及条件, 必须为数组; join可以为字符串或字符串数组.注意增加join表后, 指定主表字段时最好加上主表的固定别名"t0". 例如 res2=["b.name AS brandName", "s.name as storeName"], join="INNER JOIN CarBrand b ON b.id=t0.brandId", cond2=["t0.field1=100 and b.id IN (1,2,3)"]
subobj
目前仅限server内部使用, 要求主对象必须有id字段(未指定res/res2参数或其中有id字段). 格式为数组, 每行指定一个子对象的查询, 每行格式为: {sql, wantOne?}. "sql"指定查询语句, 其中用"%d"表示主表id; "wantOne"表示返回对象而非对象集合(数组), 缺少是对象集合. 例:
		$_REQUEST["subobj"] = [
			"items" => ["sql"=>"SELECT * FROM OrderItem WHERE orderId=%d", "wantOne"=>false]
		];
union
仅限server内部使用. 指定union查询内容, 该内容将会在以下位置影响SQL查询: "SELECT ... FROM .. WHERE ... { UNION ... } ORDER BY ...". 注意: union的结果与res参数中字段指定必须匹配; where条件必须在union中自行指定, 不可通过cond/cond2参数; orderby参数可应用到union后的最终结果.

对res, cond, orderby的安全限制:

[导出文件]

_fmt
Enum(csv,txt,excel). 导出Query的内容为指定格式。其中,csv为逗号分隔UTF8编码文本;txt为制表分隔的UTF8文本;excel为逗号分隔的gb2312编码文本(因为默认excel打开Csv文件时不支持utf8编码)。注意,由于默认会有分页,要想导出所有数据,一般可指定_pagesz=9999。

[分组统计]

gres
String. 用于groupby的字段列表。如果使用了gres字段,则res参数中每项应该带统计函数,如"sum(cnt) sum, count(id) userCnt". 最终返回列数=gres参数指定的列+res参数指定的列; 如果res参数未指定,则默认值不再是"*", 而是空(即只返回gres字段指定内容)。

例:统计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}的虚拟属性。

get/query操作中如果返回了flags/props,还会返回相应的虚拟属性;例如flags值为"vg",则多返回虚拟属性flag_v=1flag_g=1

set操作支持以下方式设置flags/props属性:

注意:

[例: 添加商户]

添加商户, 指定一些字段:

Store.add()
	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)

操作成功时无返回内容.

4.2.3 附件上传

使用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

[参数]

genThumb
Boolean. 为1时生成缩略图。如果未指定type, 则按type=default设置缩略图大小.
type
String. 商家图片上传使用"store", 用户头像上传使用"user", 其它情况不赋值. 不同的type在生成缩略图时尺寸不同。
content
文件内容。默认使用multipart/form-data格式,详见请求示例。如果fmt为"raw"或"raw_b64",则直接为文件内容(或其base64编码)
autoResize
Boolean. 缺省为1,即当图片大小超过500K, 自动缩小图片到最大像素1920x1080.
exif
Object. 扩展信息。JSON格式,如上传时间及GPS信息:{"DateTime": "2015:10:08 11:03:02", "GPSLongtitude": [121,42,7.19], "GPSLatitude": [31,14,45.8]}
fmt
指定格式,可为"raw"或"raw_b64"。这时必须用参数"f"指定文件名; 且POST content为文件内容(fmt=raw)或文件经base64编码后(fmt=raw_b64)的内容。
f
指定文件名,后台将检查其扩展名。且在fmt="raw"/"raw_b64"时使用。

type决定生成缩略图的大小:

[返回]

id
附件id. 可根据att(id)接口获取该文件。

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

4.2.4 附件下载

att(id)

根据id下载附件.

att(thumbId)

使用缩略图id获取原图. 

注意: 该调用不符合接口规范, 它不返回格式为"[code, data]"的json串, 而是直接返回文件内容.

HTTP header "Content-Type"将标识正确的文件MIME类型,如jpg类型为"image/jpeg".

如果找不到附件,将返回HTTP状态码"404 Not Found"。

[参数]

thumbId
缩略图id

[示例]

获取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

4.2.5 上传工具

upload_raw.php(f, d?=upload)(rawFileContent)

[参数]

f
上传文件名
d
上传到指定目录,默认是upload; 如果目录不存在可自动创建。
rawFileContent
HTTP请求的内容为文件内容。

可用于管理员上传文件。例如使用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: 验证管理员

4.2.6 下载工具

浏览: upload_list.php(d?=upload)

文本模式浏览: upload_list.php(d?=upload, ac=list, f)

删除文件: upload_list.php(d?=upload, ac=del, f[])

可用于手工浏览服务器上的文件。方便下载或删除文件.

注意:权限控制:只能浏览该工具所在目录及其子目录。

TODO: 验证管理员

[参数]

d
文件夹
ac
操作名,支持"del", "list"
f[]
要删除的文件列表(如"f[]=1.jpg&f[]=2.jpg"). 文件夹不可被删除。
f
文本模式浏览时, 用于过滤文件名.

[示例]

(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

4.2.7 生成签名

genSign(_pwd, ...) -> sign

根据密码对所有待签名字段进行签名。一般地,非下划线开头的字段都是待签名字段(例如_pwd, _sign这些都不参与签名),特别的会专门说明。 该API一般仅用于测试。页面 partner/voucher.html 使用了这个API。

如果希望一个参数不参与签名,则设计它的名字以"_"开头,如"_ac".

4.2.8 发短信

sendSms(phone, content, channel?=0)

[权限]

[参数]

phone
String. 一个手机号,或以英文逗号分隔的多个手机号.
content
String. 短信内容
channel
Integer. 0-验证码通道(速度快,可能被人工核查后阻止);1-营销通道(速度慢,几分钟到达,一般不阻止)

对一个或多个手机群发短信。

测试页面:tool/sms.html

4.2.9 查询短信记录

SmsLog.query([cond], [_pagesz=20])

查询短消息发送记录. 默认按id倒序排列。

[权限]

[参数]

cond
查询条件,格式参考SQL语句的条件。如"id=1", "phone=12345678901"

[请求示例] 查询手机号为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?)

给订单用户发消息,默认发微信和短信。

[权限]

[参数]

orderId
Integer. 订单编号.
content
String. 消息内容.
noWeixin
Boolean. 如果为1,则只发短信,不发微信消息。

4.2.10 代理

proxy(url)

代理访问url。

4.3 API调用监控

所有对API的调用(请求与响应)均记录到表ApiLog中供分析。

对一个session, 监控其API调用是否有异常,避免自动化操作行为,这时将返回 E_FORBIDDEN 错误。安全类异常将记录到日志文件 secure.log

注意:

4.4 批量请求

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参数必须,其它均可省略。

get
URL请求参数。
post
POST请求参数。
ref
使用了batch引用的参数列表。

如果使用事务,只是URL上加个参数:

POST api/batch?useTrans=1

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

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

5 前端应用接口

定义前端应用入口及调用参数。

每一个应用均应定义一个唯一的应用标识(app),如"emp", "emp-store"等。在调用交互接口时,框架会自动将应用标识作为参数传给后端。 应用标识中字符"-"之前的部分称为应用类型(app type),如果应用标识里没有"-",则应用类型与应用标识相同。应用类型常用于登录类型与权限控制。

例如,定义客户端应用标识app=user,其应用类型也是"user",在login交互接口中,对应用类型"user"将作用户登录处理(如查询用户表),登录成功后赋予其用户权限。 定义员工端应用标识为app=emp-store,它的应用类型是"emp",在后端将作员工登录处理(比如查询的是员工表),登录成功后赋予其员工权限。 定义管理端应用app=emp-adm,它与应用emp-store是相同类型,因而登录方式和权限是相同的,即应使用员工信息登录。

可见,不同的应用可以是相同的应用类型。在实现交互接口时,不同的应用标识也会使用不同的cookie名称,以避免多个应用同时使用时相互干扰。

5.1 移动应用

筋斗云的移动应用可做为Web应用在浏览器中运行, 也可以接入微信公众号或支付宝服务窗, 也可以通过cordova框架包装在应用容器中提供android/ios应用程序.

移动应用按惯例放在m2目录下。以常用的客户端、员工端两个移动应用为例,其相关文件有:

客户端:

商户端:

此外还有文件:

移动应用的对外接口包括页面URL,允许的入口页面(entry),URL参数等。下面举例描述这些接口。

5.1.1 客户端(app=user)

m2/index.html

这表示打开这个URL,就进入移动客户端应用。

该应用的应用标识定义为"user",表示用户登录。

[进入移动客户端并显示指定订单]

m2/index.html#order(orderId)

这表示可以请求这样的URL:

m2/index.html?orderId=32#order

其中: m2/index.html是页面地址, "?"后为参数(使用URL编码方式), "#"后为入口点, 表示允许进入的逻辑页面.

上例中的访问表示: 打开移动客户端的订单页面, 参数为orderId=32, 即显示32号订单.

5.1.2 员工端(app=emp-store)

m2/store.html

该应用的应用标识定义为"emp-store",表示员工登录(应用类型为emp)。

5.2 桌面应用

桌面应用按惯例放在web目录下。其常用文件与移动应用类似,以管理端应用为例:

常见的桌面应用示例如下。

5.2.1 管理端应用(app=emp-adm)

一般由商户员工使用,管理员工、订单等:

web/store.html

该应用的应用标识定义为"emp-adm",它与员工端(emp-store)的应用类型是相同的,都是"emp", 因而都用员工信息进行登录。

5.2.2 超级管理端应用(app=admin)

一般由超级管理员使用,甚至可执行SQL语句:

web/adm.html

该应用的应用标识定义为"admin",使用超级管理员帐号登录。注意:超级管理员帐号在用户配置文件conf.user.php中由P_ADMIN_CRED环境变量设定。

5.2.3 桌面应用查询用法

在查找对象的对话框中,可支持多种灵活的匹配方式:

例如对字段a, 填写以下值:

如果同时对多个字段填写了搜索值,则表示这些条件需要同时满足,即AND关系。

详细可参考文档 API参考 -> 筋斗云前端(桌面Web版) -> WUI.getQueryCond

5.2.4 通用参数

移动应用和桌面应用的框架支持以下通用参数:

test/_test?=0
Integer. 设置后台使用测试模式。1表示测试模式.
_debug?=0
Integer. 设置本次调用的服务端调试等级. 调试信息可在调用交互接口的返回内容中查看(返回内容的第三项): [code, ret, debugInfo]. 常用值:0-无额外信息; 1-基本信息, 9-所有信息, 包含数据库查询语句.

以下参数适用于移动应用:

cordova?=0
Integer. 0表示普通Web应用. 当网页通过[应用容器]以原生android/ios应用方式运行时, 值为非0, 表示容器的版本号.

注意: 一旦设置为非0, 则该值会被记住, 下次打开时即使未指定也会有值, 必须重新设置cordova=0清除(或在控制台中调用delStorage("cordova")).

以下参数适用于桌面应用:

autoLogin
Boolean. 如果为1, 则记住登录token, 下次打开时可以自动登录.

5.2.5 全局变量

移动应用和桌面应用使用以下JS全局变量:

g_args
Object. 应用打开时的URL参数. 由框架自动设置.
g_data
Object. 通用全局变量, 存储各项配置或应用数据. 常用项为userInfo, 表示登录后获取的用户信息.
g_cfg
Object. 全局配置。

以上变量可通过控制台手工调节部分参数.

6 测试设计

[测试需求]

所有测试内容存放在rtest目录下。

注:

6.1 手工测试

除可通过浏览器(如Chrome插件Postman)等工具进行测试外,还提供client.php工具,可分别测试每个API,如

> client.php queryseries 100

也可直接调用callsvr方法调用任意api, 例如以下调用等价于前面例子:

> client.php callsvr queryseries brandId=100

再如通用的对表的查询: 格式callsvr command [paramstr] [poststr], 其中paramstrpoststr应使用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"

注:

6.2 回归测试

6.2.1 使用方法

[前提条件]

运行服务端和回归测试:

> 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

6.2.2 测试模式设计

[实现]

rtest实现参见rtest/rtest.php,服务端实现请在server/api.php中搜索"CARSVC_TEST".

6.2.3 API测试和用例测试

[原则]

[实现]

参考file rtest.php:

static private $isIT =false;
static private $skipIT = false;
static private $skipAll = false;
private $isCritical = false; 

6.3 单元测试

必要时对某些类进行专门测试,存放在"rtest/test"目录,手工运行它们:

cd test
phpunit xxxTest.php

6.4 自动化测试

6.4.1 取短信日志最后一条

getLastLog(type)

[权限]

[参数]

f

7 登录类型与权限管理

我们使用的权限控制模型为: "用户-权限(即角色/原子权限组)-原子权限(基本权限)". (注:权限组也称"角色", 所以某些系统中也称为"用户-角色-权限"模型.)

在我们系统中, 用户主要有User和Employee两类.

权限有两类: authorization(或称permission), 以及data ownership.

在后台系统中, 对象操作类的原子权限定义是通过AccessControl类簇实现的, 每一个类(如AC1_Ordr)即是一个权限定义(包括了对象权限, 列权限及行权限). 而对于函数操作类权限是通过checkAuth(角色)显示定义: 只有指定角色的用户才能调用.

权限定义参考章节[权限说明].

8 服务端部署与升级

8.1 初始化配置

项目初始化步骤:

如果需要重新配置,可删除配置文件 php/conf.user.php后再运行本工具。

在配置文件中,很多帐户口令、密码采用base64等方式保存,可以用在线工具 http://{server}/{app}/tool/tool.php进行编解码。

8.2 升级管理

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中的设定。

8.2.1 用法

upgrade.php

缺省进入命令行交互.

8.2.1.1 交互命令

一般命令格式与函数调用类似, 也支持直接的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")

注:

[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: 支持指定编码)

noPrompt
默认导入表之前要求确认,如果指定该项为true,则不需要提示,直接导入。

一个文件可以包含多个表,每张表的数据格式如下:

# 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(第一个参数),再同上例进行添加。

8.2.1.2 非交互命令

例:

upgrade.php car_brand car_series car_model

upgrade.php all

upgrade.php upgrade

8.2.2 写升级脚本

当表结构变化时,

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

8.3 版本发布

版本发布又称“部署”或“版本上线”,是将开发版本进行构建和优化后,上传线上服务器的过程。

筋斗云框架使用webcc组件进行版本发布,对构建后的版本,也要求git进行代码库管理。 webcc提供的功能主要有:

版本发布的配置包括:

发布或上线过程很简单,直接在git bash中运行 build_web.sh 即可。 (注意:它会用到curl, bash等工具,好在git工具包中已包含这些。)

[开发版本号与发布版本号]

在构建后,online文件夹中会自动生成文件revision.txt,代表当前开发版本号。下次构建时通过检查该版本与最新开发版本间的差异,可实现差量构建(注意:其中还包含依赖文件管理)。

在上传服务器后,会自动在服务器上生成文件revision_rel.txt, 代表当前发布版本号。下次上传时,只会进行差量上传。

8.4 客户端自动升级

对于Web应用,每次浏览器打开时均已是最新版本;但是如果浏览器一直未关闭,则需要手工刷新页面才能更新。

在手机上,特别是将Web应用打包为手机原生应用后,当服务器升级后,用户必须将应用重新打开才能获得最新版本,这相当于在浏览器中刷新。 如果用户一直不退出应用(这在手机上很常见,应用会在后台一直缓存着),必须有机制能保证版本更新后可自动刷新。

筋斗云框架支持客户端自动升级,原理如下:

通过以上过程,用户不必退出应用再重新打开,就能实现版本自动升级。

9 工具接口

本节介绍目录 server/tool/ 下的工具。

注意:server/tool/目录下的工具随项目一起发布,一般通过网络访问。而tool/目录下的工具一般是命令行工具,不发布。

9.1 tool/log.php

log.php(f?=ext, sz?=2500)

查看日志(只显示最新的若干条,倒序排列)。

[参数]

f
String. 指定日志类型,缺省查看模拟接口的日志(ext.log), 还可以为"trace".
sz
Integer. 最多读取文件大小。log.php从文件结尾处读缺省3k字节,可以用sz来修改,单位为B.

[示例]

log.php

log.php?f=trace
(查看trace log)

9.2 tool/init.php

init.php(ac?)

数据库初始化或配置文件初始化。可用的ac参数见源文件内部文档。

项目初始化方法参考前面章节"服务端部署与升级"->"初始化配置".

9.3 tool/tool.php

tool.php(ac?)

具体参数见源程序内部文档。

工具包。目前支持base64编码、解码,md5编码等。

10 定期任务

tool/task.php(ac)

它作为命令行工具执行。通过crontab设置定期执行该命令。

[安装]

进入tool目录执行php task.crontab.php生成一串文本,再运行crontab -e编辑计划任务,将刚刚生成的文本复制过来即安装好。 屏幕输出到日志文件 tool/task.log.

[参数]

ac
String. 指定具体任务。定义如下。

[ac=voucher]

建议每天执行一两次。 如果优惠券已过期,设置状态Voucher.status。发现优惠券的过期日期在3天内,发短信提醒用户。

[ac=order]

建议每小时执行一次。 隔天的订单,提前2小时通知用户。

[ac=db]

每天执行一次。 备份数据库。

11 数据安全

[需求]

考虑以下灾难场景及恢复方式:

[备份建议]

[目前方案]

[注意]

12 插件机制

支持版本: v3.0

12.1 需求

12.2 插件目录结构

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/ 目录一样,包含插件的每个逻辑页面。

12.3 插件安装

将插件直接复制到plugin目录下,在plugin/index.php中添加该插件名即可。

TODO: 安装相关API和交互接口

TODO: 相关表更新 用upgrade.php

12.4 接口

12.4.1 后端PHP API

Plugins.add($pluginList);  // 添加插件
Plugins.exists($pluginName); // 判断插件是否存在

TODO: 服务端安装插件:

Plugins.install('plugin1@1.1'); -- 注册的插件, 下载到本地,解压到plugin目录下,再自动更新plugin/index.php文件。
Plugins.uninstall('plugin1'); -- 删除插件目录,再更新plugin/index.php。

12.4.2 前端JS API

MUI.initClient(); // 前端初始化,如需调用以下接口,须在muiInit事件中调用。

Plugins.exists(pluginName); // 判断插件是否存在
Plugins.list(); // 返回当前应用的插件列表

示例:

$(document).on("muiInit", myInit);
function myInit()
{
	MUI.initClient(); // 初始化客户端环境,包括插件
	...
}

// 判断和使用插件前端页面
if (Plugins.exists('plugin1')) {
	MUI.showPage('#plugin1-page1');
}

12.4.3 交互接口

返回插件列表:

initClient() -> { @plugins? }

plugins:: { name => {js?} }

TODO: 安装与卸载

addPlugin(name)
delPlugin(name)

12.5 发布与上线

应用专属插件可直接存放在plugin目录下,使用与主应用相同的代码库。

在上线时,不同的项目分别创建一个build_web.sh,根据配置不同选择不同的插件更新到服务器。 文件plugin/index.php不上线,必须手工上传服务器。

build_web.sh

export CFG_PLUGINS=plugin1,plugin2
tool/make_install.sh

通用插件使用专门的代码库维护版本。如果要加到工程中,也可以放到plugin目录下作为子模块加到应用代码库中。

注意: