请记住,设计先于编码。筋斗云框架使用DESIGN.wiki文件作为主设计文档。需要从以下几个角度来完成应用系统的设计:
需求一般通过“用例”(Use Case)来描述系统功能,定义系统有哪几类用户,以及使用本系统的主要场景。它用于指导其后的“系统建模”和“通讯协议设计”。
在示例应用“筋斗城”中,主要应用场景有:
一般建议使用UML用例图来更直观的描述需求。下面是使用StarUML工具画出的用例图:
可以将用例图链接到设计文档的概要设计中,清晰简练的描述需求。
基本功能清楚了,就可以开始做原型设计了,即设计UI界面。
原型设计可以在纸上或脑子里比划,也可以使用专业的原型设计软件如Axure等。
我们以“筋斗城商户端”的原型设计为例,列出几个主要页面。
初次打开应用后进入登录页,登录后进入“首页”:
首页下方分三栏“首页”,“订单”和“我”,点“订单”进入“订单列表”页,再点一个订单项,可查看“订单明细”:
首页中点“我”进入“个人信息”页:
首页点“商品管理”进入“商品列表”,可在左上角菜单中选择添加商品:
“商品详情”页既可用于添加新商品,也可用于编辑已有商品。
数据是应用的基础。根据需求和用例图,我们找出系统中有哪些主要对象以及之间的关系,称为数据模型设计。
数据模型将直接指导之后的“数据库设计”,以及基于对象操作的接口设计。
对重要对象应定义每个对象的中英文名字,以便在之后的开发过程中所有人使用统一的概念。
定义对象间的关系,确定对象间是一对多或多对多的关系。
一般建议通过画ER图或类图来描述,简明清晰。下面是使用StarUML工具画出的类图:
根据需求,这里列出了系统中的主要概念,比如用户(User), 订单(Ordr),商户(Store),商户员工(Employee)等。
对象连线上的”1”和”*”体现了对象间的相互关系,如
在数据模型中不必细致到每个表和字段,那些可在数据库设计中完善。
数据模型图完成后,可开始数据库设计。后期根据产品原型图或实现的需要可以对数据库设计进行增加和更新。
在筋斗云框架中,数据库设计直接使用以下方式在文档中描述,以商户、员工相关表设计为例:
商户:
@Store: id, name, dscr, createTm, expireTm
员工:
@Employee: id, uname, phone(s), pwd, name(s), perms, storeId, createTm
员工可管理的商户:
@Emp_Store: id, empId, storeId
筋斗云倡导基于设计文档的“一站式数据模型部署”,即在文档中依照规约格式来定义数据模型,再通过部署工具实现自动部署和升级数据库(详情可参考“后端框架”->“服务端部署与升级”章节)。
筋斗云框架对数据库设计文档有以下规约:
字段类型常常通过名称隐式说明, 规则如下:
字段类型还可以通过声明后缀显式说明,如:
一个综合的例子:
@Item: id, createTm, name, abstract(l), price, price2@, score#, cmtCnt&, openFlag, dscr(t)
部署工具将根据以上规约自动更新数据库。关于数据库设计的详细约定,可参考筋斗云文档“后端框架”->“数据库设计”章节。
从本节起我们进入接口设计。对于接口,我们必须描述清楚:
筋斗云框架使用的都是WEB应用,因而应用都是通过访问URL来调用的。
前端应用接口设计将描述系统中有哪些应用,每个应用URL地址及调用参数有哪些。
每一个应用均应定义一个唯一的应用标识(app),如”emp”, “emp-store”等。在调用交互接口时,框架会自动将应用标识作为参数传给后端。
应用标识中字符”-“之前的部分称为应用类型(app type),如果应用标识里没有”-“,则应用类型与应用标识相同。应用类型常用于登录类型与权限控制。
筋斗云框架支持 移动应用 和 桌面应用 两种典型的WEB应用。
筋斗云的移动应用可做为Web应用在浏览器中运行, 也可以接入微信公众号或支付宝服务窗, 也可以通过cordova框架包装在应用容器中提供android/ios应用程序.
移动应用按惯例放在m2目录下。以客户端为例,其文件名一般是:
此外还有文件:
移动应用的对外接口包括页面URL,允许的入口页面(entry),URL参数等。下面举例描述这些接口。
桌面应用按惯例放在web目录下。其常用文件与移动应用类似,以管理端应用为例:
根据需求分析,我们知道筋斗城应用中包括以下应用:
筋斗云框架还默认带有管理端的桌面应用,我们也写到设计文档中来。初步设计如下:
URL接口原型:
m2/index.html
这表示打开这个URL,就进入移动客户端应用。
客户端应用标识app=user,其应用类型也是”user”,在login交互接口中,对应用类型”user”将作用户登录处理(如查询用户表),登录成功后赋予其用户权限。
未来根据需求可能会添加一些参数,这里先举一个例子如下:
进入移动客户端并显示指定订单的URL接口原型:
m2/index.html#order(orderId)
这表示可以请求这样的URL,用于打开客户端订单页面, 显示32号订单.
m2/index.html?orderId=32#order
其中: m2/index.html是页面地址, “?”后为参数(使用URL编码方式), “#”后为入口点, 表示允许进入的逻辑页面.
在前端应用实现时,就应将order逻辑页加到允许的入口页中,且处理URL参数orderId.
URL接口:
m2/store.html
商户端应用标识被定义为”emp-store”,它的应用类型是”emp”,在后端将作员工登录处理(比如查询的是员工表),登录成功后赋予其员工权限。
一般由商户员工使用,管理员工、订单等:
URL接口:
web/store.html
定义管理端应用标识是”emp-adm”,它与商户端应用emp-store是相同类型,因而登录方式和权限是相同的,即应使用员工信息登录。
可见,不同的应用可以是相同的应用类型。在实现交互接口时,不同的应用标识会使用不同的cookie名称,以避免多个应用同时使用时相互干扰。
一般由超级管理员使用,甚至可执行SQL语句。URL接口为:
web/adm.html
该应用的应用标识定义为”admin”,使用超级管理员帐号登录。注意:超级管理员帐号在用户配置文件conf.user.php
中由P_ADMIN_CRED
环境变量设定。
交互接口描述的是前端应用如何从后端获取数据。
交互接口设计的依据是产品原型图,我们需要考虑的是,要显示一个原型页面,前端需要调用怎样的接口来获得数据。
筋斗云的前后端交互接口使用“业务查询协议”,一种基于HTTP协议的REST-RPC风格的调用规范。REST-RPC是介于RESTful和RPC之间的一种接口设计风格。
上一章节提到过,对于接口,我们需要了解其协议和原型。下面就先从这两方面简要介绍一下。
筋斗云框架是依据DACA架构规范中的业务查询协议来实现的。
业务查询协议,简称BQP(Business Query Protocol),定义业务接口设计规范及如何形式化描述业务接口,客户端怎样请求服务端业务逻辑,以及服务端如何返回业务数据。
客户端通过HTTP协议与服务端交互,调用服务端接口。 接口请求一般使用HTTP GET或POST方法,通过URL或POST内容传递参数,参数使用urlencoded编码方式,即p1=value1&p2=value2的形式;
接口返回内容使用JSON格式。传输中,参数或属性值均使用UTF-8编码。
在定义业务接口时,应使用形式化方式描述接口。每个接口均应在接口文档中规范描述,比如接口描述:
fn(p1, p2) -> {field1, field2}
其中fn为接口名,p1, p2是两个参数,->后面部分是调用成功时的返回值,使用扩展的蚕茧表示法描述。如果没有箭头后面部分,表示没有返回值,默认返回字符串”OK”.
以下假定接口调用地址为”/api.php”,该调用可以用HTTP GET请求(通过URL传参)实现如下:
GET /api.php/fn?p1=value1&p2=value2
或用HTTP POST请求实现:
POST /api.php/fn
Content-Type: application/x-www-form-urlencoded
p2=value2&p1=value1
服务端处理成功时返回内容为一个JSON格式的数组,形如 [0, data]
,其中data
的类型由接口描述定义,例如:
HTTP/1.1 200 OK
[0, {field1: "value1", field2: "value2"}]
服务端处理失败时返回内容格式为 [非0错误码, 错误信息]
,如:
HTTP/1.1 200 OK
[1, "未认证"]
业务接口包括函数调用型接口和对象调用型接口。
函数型接口名称一般为动词或动词开头,如queryOrder, getOrder等。
对象型接口的格式为{对象名}.{动作}
, 如 “Order.get”, “Order.query”等。
对象型接口支持以下标准CRUD操作:add, set, query, get, del,即对象的添加、更新、查询和删除。
本节详细内容,可参考筋斗云文档”后端框架 -> 通讯协议设计”.
或可参考github上业务查询协议定义:https://github.com/skyshore2001/daca/blob/master/BQP.md
我们以登录页为例设计接口,需要输入手机号、密码,登录成功后进入首页。
因而我们可设计登录接口如下:
login(uname, pwd) -> {id}
uname:: 员工手机号
pwd:: 员工登录密码
登录成功后,返回该员工的id.
这里的接口原型描述了接口的参数和返回值。
登录后,在首页和个人信息页,需要员工名称、手机号、当前商户名等信息。
可设计一个接口返回这些信息,比如
getEmployee() -> {name, phone, storeName}
name:: 员工姓名. 来自Employee.name.
phone:: 员工手机号. 来自Employee.phone.
storeName:: 当前商户名. 根据Employee.storeId关联到Store.name.
应用逻辑:
- 接口权限:AUTH_EMP (员工登录后才可用)
- 服务端根据当前session已知员工id, 因此无须传参员工id。
这里不仅描述了接口原型,也指明了诸如权限检查、对应底层字段等应用逻辑,用于指导之后的实现。
我们可以发现,这个接口其实就是典型地获取Employee对象的信息,更好的设计应该是重用通用对象调用接口,如:
Employee.get(id?) -> {id, name, phone, ..., storeName?}
storeName:: 当前商户名,关联Store.name.
应用逻辑:
- 接口权限:AUTH_EMP
- 如果未指定参数id, 则使用当前session中的员工id.
- 不可返回pwd字段。
注意:
这样,我们用Employee对象的get操作替代了之前单独设计的getEmployee接口。
标准get接口支持res参数,用于选择返回字段,比如:
Employee.get() -> {id, name, phone}
Employee.get(res=*,storeName) -> {id, name, phone, storeName}
Employee.get(res=name,storeName) -> {name, storeName}
在设计接口时,我们应尽量利用通用对象接口。
通用对象操作完成对象的增删改查(CRUD)动作。 一般一个对象对应一张数据库主表,若干子表以及若干关联表。
对象名即主表名,接口如下:
{object}.add()(POST fields...) -> id
{object}.set(id)(POST fields...)
{object}.get(id, res?) -> {fields...}
{object}.query(res?, cond?, orderby?) -> tbl(field1,field2,...)
{object}.del(id)
对于add/set方法, 须使用HTTP POST请求; POST内容是对象字段及其值,如a=1&b=2
。其它操作既可以用HTTP GET请求,也可以用HTTP POST请求。
set操作中的id参数是对象的整型主键,不可修改,须通过URL参数传递。
get/query操作是比较类似的,分别返回一个对象和对象列表。
query操作非常灵活,除基本的查询外,还支持分页、分组统计、导出等。
查询操作的参数可参照SQL语句来理解:
尽管类似SQL语句,但对参数值有一些安全限制:
用参数cond
指定查询条件, 如:
cond="type='A' and name like '%hello%'"
以下情况都不允许:
left(type, 1)='A' -- 条件左边只能是字段,不允许计算或函数
type=type2 -- 字段与字段比较不允许
type in (select type from table2) -- 子表不允许
query操作返回table结构,如
{
"h": ["id", "name"],
"d": [[1, "liang"], [2, "wang"]]
}
[例: 添加商户]
添加商户, 指定一些字段:
Store.add()
name=华莹汽车(张江店)
addr=金科路88号
tel=021-12345678
注:
Store是商户表名, 通过POST字段传递各字段内容. HTTP POST请求如下所示(实际发送时, 每个字段的值应使用UTF8+URL编码, 示例中未进行编码):
POST /api.php?ac=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": [
[ 7, "华莹汽车(金桥店)", "上海市浦东区金桥路1100号"],
[ 8, "华莹汽车(张江店)", "金科路88号" ]
]
}
[例: 更新商户]
为商户设置描述信息等:
Store.set(id=8)
opentime=8:00-18:00
dscr=描述信息.
操作成功时无返回内容.
[例: 删除商户]
Store.del(id=8)
操作成功时无返回内容.
上述例子中,很多返回了JSON格式的复合类型,主要有对象、数组等,在原型中使用了”蚕茧表示法”来描述数据结构,常用的列举如下:
{id, name}
一个简单对象,有两个字段id和name。例:{id: 100, name: "name1"}
[id…]
一个简单数组,元素为id。例:[100, 200, 400]
, 每项为一个id
[id, name]
一个简单数组,例:[100, "liang"]
,第一项为id, 第二项为name
[ [id, name] ] 或 varr(id, name)
简单二维数组,又称varr, 如 [ [100, "liang"], [101, "wang"] ]
.
[{id, name}] 或 objarr(id, name)
一个数组,每项为一个对象,又称objarr。例:[{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"]]
}