概述
Project-Script是一个面向开发人员的代码生成工具,基于DaaS框架,只有一个jar,可以生成包括 页面流,查询,状态机,表单 在内的各种代码框架,从而保证开发人员能够最大可能的把精力聚焦在业务内容上。这个符合 DaaS 的目标:写的不是代码,是业务
初始化
提供的jar包中有一个类,ProjectSetup 用来帮助生成 Project-Script 的代码目录。 一个代码例子如下图:
其中代码主要部分摘录如下
ProjectSetup setup = new ProjectSetup();
// 生成到哪里
String outputBaseFolder = getWorkSpaceFolder(projectFolder);
setup.setProjectFolder(outputBaseFolder);
// 项目名称
setup.setPackageName("com.doublechaintech."+modelName);
List<GenrationResult> list = setup.runJob();
// 执行
String baseFolder = outputBaseFolder+"/code-gen-client/project_"+modelName;
setup.saveToFiles(new File(baseFolder), list); // "changeReque
// 完成
return;
它运行的结果就是生成了如下的代码结构:
主入口
代码位置
在生成的代码中,很明显,执行的入口是 Main.java定制代码
它的内容很直白看内容就很容易理解其目的。 如果需要改变其中的参数,直接修改即可。 因为这些工作是一次性的,以后用版本管理就好了。
页面流
页面流写法一:使用java脚本
先看一个例子
// 第一步, 点击'上架'. 可能是作品里的'我要卖', 也可能是'可售'列表直接点击, 也可能是'上架'
.request("put product on sale v3").with_string("artwork id")
.comments("2019-11-6:按v3新规则开始上架作品").no_footprint()
.when("lack of deposit").comments("店铺保证金不够")
.got_popup_page().comments("请求用户确认补保")
.may_request("start to recharge shop deposit for new auction")
.when("quick reshelf").comments("快速二次上架")
.got_popup_page().comments("提示用户新拍卖的参数,请求确认")
.may_request("confirm quick reshelf auction")
.when_others().comments("正常情况下")
.got_page("artwork auction application").comments("艺术品上架销售表单")
.may_request("submit artwork auction application")
这个语法很直接
.request("put product on sale v3").with_string("artwork id")我要发送一个请求,它叫做“put product on sale v3”, 带一个参数 “artwork id” .comments("2019-11-6:按v3新规则开始上架作品").no_footprint() 注释‘2019-11-6....’, 这请求不会留下脚印 (back的时候回跳过它) .when("lack of deposit").comments("店铺保证金不够") 如果这个请求的处理结果是 lack-of-deposit .got_popup_page().comments("请求用户确认补保") 那么,显示一个 popup窗口 页面 .may_request("start to recharge shop deposit for new auction") 在这个页面上,有个请求“start to recharge ...” .when("quick reshelf").comments("快速二次上架") 如果这个请求的处理结果是 quick-reshelf .got_popup_page().comments("提示用户新拍卖的参数,请求确认") 那么显示一个 popup窗口 页面 .may_request("confirm quick reshelf auction") 这个页面上有个 ‘confirm quick ...’ 的请求
.when_others().comments("正常情况下") 在其他(也就是正常)情况下 .got_page("artwork auction application").comments("艺术品上架销售表单") 打开页面‘artwork auction application’页面 .may_request("submit artwork auction application") 这个页面上可以发请求:submit artwork auction application
页面流写法二:使用http://editor.doublechaintech.com/
我们提供了一个内部工具,帮助描述页面流。 导出的文件可以直接导入。
在前面自动生成的代码里,可以看到导入的部分:
这个工具中绘制的页面流如下所示
它和java脚本的作用是一致的,两个都可以单独使用, 也可以工具为主,java脚本做补充。
使用绘图工具的好处就是全局视角,容易理解;
java脚本的好处就是可以使用java的任意技术,例如大批量的类似请求,java就方便,绘图就麻烦。
页面流生成的代码,默认有4个
1. BaseXXXViewService
'视图服务'基类,包含常用工具方法,URL定义,接口声明,常量定义等。
自动生成,每次生成会被覆盖。
2. XXXViewService
'视图服务'类。各接口的默认实现。
自动生成,每次生成会被覆盖。
3. BaseXXXBizService
'业务’基类,一些公共方法可以放在此处。
只会生成一次,不会被覆盖。
4. XXXViewBizServer
'业务’实现类。基本上主要是在此类中进行开发
只会生成一次空文件,不会被覆盖。
查询
一个查询的例子
return script
.query(MODEL.employee()).list_of("colleague in merchant").with_string("merchant id").with_string("personal id")
.comments("查询用户在某个公司内的正式同事")
.do_it_as()
.where(MODEL.employee().personInformation().not("${personal id}"),
MODEL.employee().merchant().eq("${merchant id}"),
MODEL.employee().status().in(EmployeeStatus.NORMAL))
.order_by(MODEL.employee().personInformation().name()).asc_by_pinyin()
.wants(MODEL.employee().role(), MODEL.employee().personInformation())
MODEL 对象是自动生成的,它包含所有的模型定义。
上面的例子,可以描述为
查询‘模型employee’,的‘在某个公司内的正式同事’的列表,带参数 merchant-id和personal-id
它的内容是:
条件: employee的个人信息不等于输入的参数 personal-id
而且 employee的商家等于输入参数 merchant-id
而且 employee的状态是 NORMAL
排序规则是: employee的个人信息的姓名,按照拼音升序
查询结果中,还希望它同时加载 employee的角色,以及它的 个人信息
查询生成的代码在这里
状态机定义
状态机目前还是内部试用阶段。它是独立生成的,可以根据情况选择使用.
还是先看一个例子
语法也是很直白的
.in_status("xxx state").zh_CN("XX状态")
.comments("...")
.on_event("xxx event").zh_CN("xxx 事件")
.when("xxx").zh_CN("xxx处理结果").reach_condition("xxx")
.when("yyy").zh_CN("yyy处理结果").go_to("yyy state")
.on_event("...
.when_condition("xxx").zh_CN("xxx条件").go_to("xxx state")
.as_role("xxx actor").zh_CN("XXX工").can_do_nothing()
.as_role("yyy actor").zh_CN("YYY工").can_do("xxx")
状态机定义
一个状态机,一般定义如下内容- 状态
- 事件
- 分支代码
- 状态迁移路线
- 参与角色
- 指定角色在指定状态下的可执行操作代码
定义状态
语法如下
.in_status("<english name>").zh_CN("中文名")
注: .once_started() 等价于 .in_status("start")).zh_CN("启动")
定义事件
语法如下
on_event("english name").zh_cn("中文名")
定义分支代码
Project-Script 目前只定义代码框架,不直接处理具体的业务。
对事件的处理结果不同,会导致状态机的流转不同。 框架代码和实际业务代码之间,就是通过‘分支代码’(branch code)来约定各种处理结果的。
语法如下
.when("branch code").zh_CN("分支名称")
注 .when_success() 等价于 when("ok").zh_CN("成功")
定义条件
条件 是指分支时进行判断的依据。
条件可以显式定义,也可以让脚本自动推断。 例如:
.when("xxx").zh_CN("xxx处理结果").reach_condition("<条件代码>")
就是表示:当前事件的处理结果为 xxx 时,会达成 <条件代码> 中指定的条件。
.when("xxx").zh_CN("xxx处理结果").go_to("XXX")
则表示: 当前事件的处理结果为 xxx 时,(推断其)达成条件 xxx,将会去到 XXX 状态.
.once_enter().go_to("xxx")
则表示:当进入状态后,(推断其)达成条件‘ENTER’,将会去到 xxx 状态
定义状态迁移路线
标准的语法是
.when_condition("<条件>").zh_CN("<条件中文名>").go_to("<目标状态>")
也可以利用自动推断,直接在分支定义后跟迁移目标:
.when("xxx").zh_CN("xxx处理结果").go_to("<目标状态>")
定义角色
语法如下
.as_role("<english name>").zh_CN("<角色名>")
指定角色在指定状态下的可执行操作代码
语法如下
.as_role("xxx actor").zh_CN("XXX工").can_do_nothing() // 无可行操作
.as_role("yyy actor").zh_CN("YYY工").can_do("<操作代码>") // 可以执行 <操作代码> 对应的动作
操作代码 只是一个字符串,用于在系统各个模块之间沟通。 通常它用于展示某个状态下,页面上有哪些操作按钮。
代码定制
生成的代码位置如下
如上图所示,我们定义了两个业务流:Order 和 Shipping
- XxxProcessSpec 状态机规格文件
- BaseXxxProcessor 状态机基类文件。 主要任务是生成了各个onEnterXXXStatus, onLeaveXXXStatus, onConditionXxx, handleEventXxx 函数
- XxxProcessor 状态机真正的业务实现类。 主要是在这里处理业务
定义表单
在DaaS中,称多步表单为 ChangeRequest。 普通的业务请求,会导致核心业务数据发生变化的,都应该通过ChangeRequest来完成。
先看一个例子
Project-Script中的CR的层级结构
大多数情况下其实一个ChangeRequest里只有一步,一组信息,此时可以简单的认为它就是一个表单。
定义复用
ChangeRequest的定义可以复用,只需要contains_event指向相同的定义,然后再单独描述有差异的地方即可。
例如:
在这个例子中,seller update merchant info 和 update merchant info when invite 都使用“organization info”,而它的定义中,有一个字段“agency social code”和初始定义不同,需要变成可选字段。所以这里就只描述它为 optional的
定制开发
生成的代码为 XxxChangeRequestHelper.java
同时生成一个 ChangeRequestHelper.java, 其中只有一个of(context)方法,实际的开发工作在这个类里进行。
可以根据需要,对字段名,候选值等进行定制化。
哪些方法可以定制化
DaaS的特征之一,就是提供全部源代码,所以, 你想改哪里就改哪里。o( ̄︶ ̄)o
常用的重载点有
afterFieldFulfilled: 这个是字段被OOTB填完以后,追加一些自己的数据
afterCRDataFulfilled: 这个是整个CR填完以后,追加一些自己的处理
调用时机可以直接在源代码里看。
拿到字段的方法可以这样写:
CRGroupData group = crData.group(xxCR.XXX.SCENE_X.GROUP_X.NAME);
CRFieldSelector fd = group.tryField(null, xxCR.XXX.SCENE_X.GROUP_X.FIELD_xxx)
.hidden()
.value(xxx)...;
// 或者直接拿到字段
CRFieldData xxField = fd.get();
// 然后直接操纵字段。 CRFieldSelector只是个builder风格的包装器。
附录
表单中,地址选择字段 如何填写
地址选择是个复杂行为,包括 省/市/县 这样的分级内容以及交互:选择->回填多级参数等。 默认的,DaaS支持3级行政区域选择,在模型文件中包含:<#import file="../lib/province_city_and_district.xml" />
注:import 功能目前仅在企业版开放,使用免费版的可以自己将以下内容加入模型文件中
<root
cfg_import_zone="chinese_level_3"
>
<!-- 省 -->
<province
_name="省/直辖市/自治区"
name="名称:四川|北京|[1,120]"
platform="$(platform)"
_features="setting"
/>
<!-- 市 -->
<city
_name="城市"
name="名称:成都|北京|[1,120]"
province="$(province)"
platform="$(platform)"
_features="setting"
/>
<!-- 区 -->
<district
_name="区"
name="名称:成华区|朝阳区|锦江区|海淀区|[1,120]"
city="$(city)"
platform="$(platform)"
_features="setting"
/>
这样就会生成一个文件:
com.xxx.yyy.utils.AddressImportUtil.java
这个工具类主要包含两个方法
// 从指定文件导入省市县数据到数据库
public void importAddress(XXXContext ctx) throws Exception;
// 从数据库加载所有的省市县层级数据
public Object makeRegionList(XXXUserContext ctx) throws Exception;
和DaaS一贯的风格一致,这个文件是以源码形式提供的,可以根据需要修改。 此文件只生成一次,如果存在就不再覆盖。
importAddress
这个用于向数据库导入初始数据。 默认的,在项目文件夹里有一个文件 ok_data_level3.csv。 在导入的时候,有这么一句:String fileName =
EnvUtil.getValueFromEnv(getFileName(),
"CITY_DATA_FILE",
path + "../../ok_data_level3.csv");
也就是说,优先使用属性 fileName, 如果没有,从环境变量 CITY_DATA_FILE 取,还没有了就从当前项目目录主目录下的ok_data_level3.csv取。
开发人员在初始化系统基础数据时,可以用此方法导入所有的行政区域数据。
makeRegionList
默认的,前台和后台约定的查询行政区域的接口是
/<content path>/<service bean>/makeRegionList/
DaaS默认在BaseManagerImpl中有这样的一个函数:
public Object makeRegionList(Xt20UserContext userContext) throws Exception {
return new AddressImportUtil().makeRegionList(userContext);
}
所以只要继承BaseManagerImpl就支持这个接口。 当然,你也可以根据项目需要修改。
ChangeRequest中的声明
在change request 中声明一个字段
.has_field("district").zh_CN("地区")
.which_model_of(MODEL.district())
.with_style(UIStyle.INPUT_REGION_PICKER)
.place_holder("请选择所在地区")
这个类型 UIStyle.INPUT_REGION_PICKER 会告诉前台这个字段需要特殊处理,去调用约定的例如makeRegionList这个接口来获取候选值。
这个声明将会只保存最后的 district (在模型中声明的)数据,因为从它可以追溯到市和省。 默认实现只有选择,没有回填。 回填的场景需要和前台约定好如何填写数据。
默认的会回填前台传回来的原始数据。那么第一次就没有数据,在有些场景下需要一上来就有一个默认的选择,此时需要手工定制化这个数据的预填充。
定制方法是在 xxxChangRequestHandler.java 中,重载方法
@Override
protected Object getFieldValueWhenFillResponse(
Object suggestValue,
GenericFormPage requestData,
ChangeRequest changeRequest,
CRGroupData groupData,
CRFieldSpec fieldSpec) throws Exception
在这个函数中根据fieldSpec判断是否是你需要定制的字段。例如
switch (fieldSpec.getName()) {
case WxappServiceCR.APPOINTMENT_APPLICATION.FIELD_XXX: {
...
来编写你的代码