Daas 最佳实践-ChangeRequest数据交换

540 阅读12分钟

(草稿,未发布)

前言

ChangRequest 是 DaaS 最佳实践的一部分。

ChangeRequest(以下简称CR) 用于规范化前端与后端的 ‘变更类’ 交互,例如提交form,确认某项重要决策。 CR的主要特性包括:

  • 所有请求有据可查

    无需特别代码,DaaS会自动记录CR的所有数据,包括请求来源,当前用户,请求内容;

  • 接口一致性

    CR 是结构化数据,无论手机端,PC端,REST,都只需遵循CR数据规范,即可获得后端的服务。

    一致性不仅提现在多种前端,还提现在不同版本,不同项目,不同团队...总之有了规范,才有了自由。

  • 接口扩展性

    DaaS的最佳实践,是通过团队诸君,多年来工程实践的分析,建立的一套性价比极高的‘观点’,应对多年来诸多项目的实际开发、维护任务中各种细节问题。版本升级后,对外服务接口的升级和兼容是其中很大一个。CR将数据和 元信息 分离,规范化的结果就是处理起来责任明确,简单易理解,维护成本降低.

CR 的逻辑数据结构

IT(information technology)的核心,毫无疑问是 信息。 DaaS的观点是:变更类的请求处理,就是对外部信息的反应(好像是大实话,没什么意义^_^)。而外部信息进入系统,就是一个‘事件’。

而‘外部信息’打包在一起分类一下,就包括这么几类:

  • 内容信息:xxx发了什么信息?例如‘十斤肥肉不要一点瘦的,十斤廋肉不要一丝肥的,剁成臊子’
  • 身份信息:谁发起的?是鲁提辖还是普通人,您得到的响应想必是不同的, 吧?
  • 目标信息:向谁发起的?向镇关西,还是向听众?CR中,相同的数据,提交给不同的接口,会得到不同的处理。

其实就是5W1H的具现:Who, What, When, Where, Why, and How。

其中what 是内容信息

Who when 是身份信息

where 是目标信息

why 是隐含信息,既体现在前台展现的内容上,也提现在后台逻辑上;How 是业务信息,一般我们(程序员)要处理的就是这个部分。

所以CR的处理第一步,就是约定‘外部信息’到底是个什么样子的。

外部信息可能简单到‘好的’,也可能复杂如‘个税申报’那样。

DaaS的CR认为,外部实际上有一个‘信息收集’的过程,所以CR在逻辑上是一个树形结构

ChangeRequest             :CR 请求
   + Step(s)              :收集信息可能有多步
      + Event(s)          :每次会收集一类相关的信息
          + Field(s)      :每个信息里有一个或多个具体的值

在DaaS中,信息结构可以从代码中一窥:

public interface OutputName {
String NAME = "name";
String TITLE = "title";
String CHANGE_REQUEST_LIST = "changeRequestList";

interface CHANGE_REQUEST {
	String NAME = "name";
	String TITLE = "title";
	String TYPE = "changeRequestType";
	String STEP_LIST = "stepList";

	interface STEP {
		String NAME = "name";
		String TITLE = "title";
		String INDEX = "index";
		String SKIP = "canSkip";
		String EVENT_LIST = "eventList";

		interface EVENT {
			String NAME = "name";
			String TITLE = "title";
			String TYPE = "eventType";
			String MUST = "mustHave";
			String MULTIPLE = "multiple";
			String MIN = "minimalNumber";
			String MAX = "maximumNumber";
			String FIELD_LIST = "fieldList";
			int _DEFAULT_MAX = 500;
			
			interface FIELD {
				String NAME = "name";
				String TITLE = "title";
				String INTER_ACTION_MODE = "interactionMode";
				String TYPE = "inputType";
				String SELECTABLE = "selectable";
				String MUST = "mustHave";
				String MIN = "minimal";
				String MAX = "maximum";
				String PLACE_HOLDER = "placeholder";
				String TIPS_TITLE = "tipsTitle";
				String TIPS_CONTENT = "tipsContent";
				String SELECTABLE_NOT = "not_selectable";
				String SELECTABLE_SINGLE = "single_selectable";
				String SELECTABLE_MULTI = "multi_selectable";
				String FORCE_VALUE = "value";
				String DEFAULT_VALUE = "defaultValue";
				String VALUES = "valuesMapping";
				String VALUES_RETRIEVE_API = "valuesRetrieveApi";
				String SAMPLE_DATA = "sampleData";
			}
		}
	}
}
}

以上为代码示例(截取于2020-3-10),非正式release代码。

CR 的交互数据

最直接的做法,是每次都将完整的CR信息,按照逻辑结构传递给后台。

这个简单直接,但是有点麻烦,工作量大,而且容易出错。

分析一下数据,可以发现,实际上‘CR类型’可以表达很多'固定'的信息。

例如,我提交一个请求,https://...?name=张三 , 那么你知道是什么吗?不知道。如果我说 https://...?CR=更新用户信息&name=张三 那您就猜了:这个应该是把当前用户的名字改了吧?

这个CR=更新用户信息就代表了很多信息,而且这些信息在整个系统中是不会变化的 固定 信息

上面代码中展示的信息定义,其实大部分都可以用“我是哪个CR第几步哪个Event的第几条的哪个字段” 来表明

name=张三 加一句:这是‘更改用户信息’的,唯一一步的,‘用户新身份信息’的,唯一一条的‘姓名’字段 。 这样就非常清楚了。

所以DaaS中的交互数据包括两部分:一部分是‘CR specification’, 一部分是‘CR data’。

CR 的规格定义

  • 不要着急,后面会知道的

CR 的接口及交互数据说明

前面说过,CR将数据和原信息分开了,这个直接看代码比较直观

public class ChangeReqeuestData implements RemoteInitiable{
	protected ChangeRequestMetaInfo requestInfo;
	protected List<Object> requestData;
	...
}

public class ChangeRequestMetaInfo {
	protected String changeRequestType;
	protected String changeRequestId;
	protected String stepName;
	protected String eventLocation; // only used when multiple events needed in current step
	protected boolean loadAllEventsInCurrentStep = false; // by default, only load 'current' event in a step
	protected String version;	// format: (major).(minor).(debug).(build)
	protected String action;	// only user by front-end
	...
}

CR 的交互数据包含

  • requestInfo: 这个是 ChangeRequestMetaInfo 类型的数据
  • requestData:就是前面说的event的list

还有个问题:每个event怎么知道前面说的 “我是哪个CR第几步哪个Event的第几条的哪个字段”

Event 的数据中有几个约定的字段:

此图为DaaS模型定义截图,详细描述请见文档: kdocs.cn/l/sS9sjXdPo… (DaaS 概要与建模.docx)

  • event_location 就是用来标明它在本次CR中的地位的
  • change_request 是用来标明具体哪次CR请求的

开始CR - 无未完成的情况

前端要求开始一个CR,且当前用户历史上从未填写过该CR,或者全部已完成。(可议:是不是完成的最后一个CR也当做‘默认值’加载出来?)

  • 入口

    CR总是从某个页面开始的,此页面由业务定义,无特定参数要求。

  • 返回

    业务知道此请求的CR类型以及是谁发起了请求,所以后端可以据此创建一个空的新CR。

    一个空的CR样例如下

      {
          "requestInfo": {
              "changeRequestType": "MARRIAGE_REGISTRATION",
              "changeRequestId": "CR000005",
              "stepName": "personal_info",
              "loadAllEventsInCurrentStep": false
          },
          "requestData": []
      }
    

    讨论: 如果没有数据,是传一个空requestData,还是构造本步骤中所有默认的event传回来?

开始CR - 有未完成的情况

前端要求开始一个CR,且当前用户历史上曾经有填写过该CR,但是未完成。

  • 入口

    同上

  • 返回

    业务知道此请求的CR类型以及是谁发起了请求,所以后端可以据此找到当前用户未完成的CR。

    一个有历史数据的CR样例如下

      {
        "requestInfo": {
            "changeRequestType": "MARRIAGE_REGISTRATION",
            "changeRequestId": "CR000010",
            "stepName": "personal_info",
            "loadAllEventsInCurrentStep": false
        },
        "requestData": [
            {
                "id": "EC000023",
                "name": "张先生",
                "gender": "male",
                "birthday": 10944000000,
                "eventLocation": "/MARRIAGE_REGISTRATION/personal_info/male/1"
            },
            {
                "id": "EC000024",
                "name": "赵女士",
                "gender": "female",
                "birthday": 338918400000,
                "eventLocation": "/MARRIAGE_REGISTRATION/personal_info/female/1"
            }
        ]
      }
    

注意上面 requestData 中多出来的数据。

其中有个字段‘eventLocation’ 是DaaS自动填写的,例如上面标明这两个event是“婚姻注册的个人信息的男/女信息的第一条(应该也是唯一一条)”

提交CR - 下一步

前端开始一个CR后,获得了前面所说的基本信息,可以据此信息,以及CR的规格数据,渲染成一个form。
用户填写数据后(假设没有多个event,只有下一步),点击下一步。 此时前台需要构造一个 ‘下一步’的action。

  • 入口

    业务约定的API url。例如 https://.../commitRegistrationCR/

    这里需要使用 PUT ,body内容为:

       {
        "requestInfo": {
            "changeRequestType": "MARRIAGE_REGISTRATION",
            "changeRequestId": "CR000010",
            "stepName": "personal_info",
            "loadAllEventsInCurrentStep": false,
            "action": "next_step"
        },
        "requestData": [
            {
                "name": "张先生",
                "gender": "male",
                "birthday": 10944000000,
                "eventLocation": "male/"
            },
            {
                "name": "赵女士",
                "gender": "female",
                "birthday": 338918400000,
                "eventLocation": "female/"
            }
        ]
      }
    

说明:

  • requestInfo 的大部分信息是后台之前提供的数据,原样填充
  • requestInfo.action 用来表示此次操作的行为是‘下一步’
  • requestData 中是用户填写的数据。假设是新数据,所以没有ID
  • requestData 中每条数据必须有 eventLocation

    eventLocation 可以用‘绝对路径’,也可以用‘相对路径’

    例如前面例子中的 “/MARRIAGE_REGISTRATION/personal_info/male/1” 就是一个绝对路径,可以直接了解到它是哪个Event,在哪个CR的第几步收集的。

    实际上大多数情况下相对路径就足够了。 例如 “female/”, 因为在requestInfo中我们知道它的 changeRequestType 和 stepName,所以信息是充足的。

    在数据库中存储的时候,DaaS会自动将提交时的相对路径,转换成绝对路径,因为数据库中是没有上下文的。

    如果规格文件中定义了此Event是可以有多个的,那么路径的最后一小节是表示序号的。例如 'female/1' 表示这是女方信息的第一条记录。

    如果规格文件标明此Event是单个的,那么会忽略最后一小节,前后台交互的时候,此字段可填可不填

    如果此Event是单个的,那么无论action是什么(除了delete),后台都会做 createOrUpdate 操作

    如果此Event可能有多个,那么后台是新建数据,还是更新数据,则有更详细的规则,后面讲到具体场景会说。

  • 返回

    后台收到‘下一步’的action后,会根据CR的规格文件计算,判断如何处理提交的数据,处理完成后,会再计算下一步应该做什么,有几个可能:

    • 下一步还有信息要收集: 那么就返回新的数据,格式和开始CR - 无未完成的情况 或者 开始CR - 有未完成的情况 一样

        {
          "requestInfo": {
              "changeRequestType": "MARRIAGE_REGISTRATION",
              "changeRequestId": "CR000010",
              "stepName": "premaritial_notarization",
              "loadAllEventsInCurrentStep": false
          },
          "requestData": [
              ... 有或者没有数据 ...
          ]
        }
      

      注意 stepName 变成了 premaritial_notarization, 标明当前是‘婚前公证'这一步的信息收集了。

提交CR - 下一条

当Event在一个步骤中可以有多条时,应该会有‘记录’级别的操作,例如:

此图为DaaS的ChangeRequest校验工具生成的页面截图,用于快速浏览规格是否符合业务需求。

‘补充条款’的规格是 0~200,也就是可以有多条。那么,它通常应该会有上一条,删除和下一条3种操作。

  • 入口

    业务约定的API url。例如 https://.../commitRegistrationCR/

    这里需要使用 PUT ,body内容为:

       {
        "requestInfo": {
            "changeRequestType": "MARRIAGE_REGISTRATION",
            "changeRequestId": "CR000010",
            "stepName": "personal_info",
            "eventLocation": "male/1"
            "loadAllEventsInCurrentStep": false,
            "action": "next_record"
        },
        "requestData": [
            {
                "name": "张先生",
                "gender": "male",
                "birthday": 10944000000,
                "eventLocation": "male/"
            }
          ]
    

    说明

    注意requestInfo中多了一个参数 eventLocation。 此参数在前面ChangeRequestMetaInfo中已经展示了,因为后台返回数据时此值为空,所以序列化结果中不包括。 但是在请求时需要加入。 eventLocation 的规则前面已经说过。这里它表示‘我当前操作的Event’。

    注意此时CR包含两种数据:

    • 我现在的数据: 包括下面的requestData 和 eventLocation 都是说现在这个请求里的事情;
    • 我需要的未来的数据:由 action=next_record 来表示。

    简单来说,这个请求表示:

    1. 我现在处于 MARRIAGE_REGISTRATION/personal_info/male/1 这条记录
    2. 此条记录我已经修改为 requestData 中所提供的值;
    3. 我要更新/创建 MARRIAGE_REGISTRATION/personal_info/male/2 这条记录
  • 返回

    后台收到‘下一条’的action后,会根据CR的规格文件计算,判断如何处理提交的数据,处理完成后,会再计算下一步应该做什么,有几个可能:

    • 下一条已有信息: 那么就返回已有的数据,格式和开始CR - 有未完成的情况 一样

    • 下一条还没有:那么就返回空的数据,格式和开始CR - 无未完成的情况

        {
          "requestInfo": {
              "changeRequestType": "MARRIAGE_REGISTRATION",
              "changeRequestId": "CR000010",
              "stepName": "personal_info",
              "loadAllEventsInCurrentStep": false
          },
          "requestData": [
              ... 有或者没有数据 ...
          ]
        }
      

      注意 stepName 保持 personal_info 不变, 标明当前是还在这一步,只是还有更多信息要收集。

修改CR - 上一步

当用户希望修改曾经填写过的数据时,需要先找到这个数据,因此需要‘上一步’来回到前面填写的某步骤。

‘上一步’有几个特点:

  • 当前页面填写的数据,被废弃;
  • 请求比‘下一步’简单,不一定需要PUT方法(因为没有要保存的数据)
  • 入口 业务约定的API url。例如 https://.../commitRegistrationCR/, 同时使用PUT请求

    或者

    业务约定的API url。例如 https://.../openRegistrationCR/<CR-ID>/<event-location>/action/,

    例如https://.../openRegistrationCR/CR000088/MARRIAGE_REGISTRATION%2fpersonal_info%2fmale%2f2/prev_step/

    MARRIAGE_REGISTRATION%2fpersonal_info%2fmale%2f2 是 MARRIAGE_REGISTRATION/personal_info/male/2 编码的结果

    如果采用第一种,PUT 方法,那么参数和前面几乎一致

      {
        "requestInfo": {
            "changeRequestType": "MARRIAGE_REGISTRATION",
            "changeRequestId": "CR000010",
            "stepName": "personal_info",
            "loadAllEventsInCurrentStep": false,
            "action":"prev_step"
        }
      }
    

    如果用第二种 GET 方法,参数解释如下:

    • CR-ID: 当前CR的ID
    • event_location: 当前命令的出发点的定位。必须使用绝对路径
    • action: 打开方式。可以是:
      • prev_step: 上一步
      • prev_record: 上一条
      • right_there: 直接打开event_location所对应的记录
      • next_step: 下一步 (下个版本支持)
      • next_record: 下一条 (下个版本支持)
  • 返回

返回的数据结构同前。注意几个规则:

  • 如果当前指定的event—location不存在
    • '上一步' 返回当前CR规格文件中定义的,从‘假如’存在的当前‘步骤’向前追溯的,可得到的最后一步;
    • 如果还是找不到,最早返回到第一步。
    • '上一条' 先判断当前指定的’步骤‘是否存在,不存在按照上面的规则追溯至第一步;
    • 当前步骤存在,而当前记录不存在,根据event_location中指定的event——index向前追溯,最多到第一条。

返回的数据如:

{
  "requestInfo": {
      "changeRequestType": "MARRIAGE_REGISTRATION",
      "changeRequestId": "CR000010",
      "stepName": "personal_info",
      "loadAllEventsInCurrentStep": false
  },
  "requestData": [
      ... 有或者没有数据 ...
  ]
}

修改CR - 上一条

'上一条' 和 ‘上一步’ 的参数格式完全一致,只是action=prev_record

修改CR - 删除

  1. 只有当前步骤中的某个event有多条时,才可以执行 删除;
  2. 只有删除记录的操作,没有删除step的操作
  • 入口

未完待续