(草稿,未发布)
前言
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 来表示。
简单来说,这个请求表示:
- 我现在处于 MARRIAGE_REGISTRATION/personal_info/male/1 这条记录
- 此条记录我已经修改为 requestData 中所提供的值;
- 我要更新/创建 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 - 删除
- 只有当前步骤中的某个event有多条时,才可以执行 删除;
- 只有删除记录的操作,没有删除step的操作
- 入口
未完待续