DaaS 项目脚本(project script)工具使用介绍

683 阅读10分钟

概述

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

  1. XxxProcessSpec 状态机规格文件
  2. BaseXxxProcessor 状态机基类文件。 主要任务是生成了各个onEnterXXXStatus, onLeaveXXXStatus, onConditionXxx, handleEventXxx 函数
  3. 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: {
			...
            

来编写你的代码