This repository has been archived on 2025-10-29. You can view files and clone it, but cannot push or open issues or pull requests.
Files
docs/llms-full.txt
2025-08-10 16:34:51 +08:00

1445 lines
109 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<TOCO知识库>
-----------------------------------------------------------------------------
### **1. TOCO 平台概览:**
- **1.1 平台简介:** TOCO是一款重视软件设计及核心代码自动生成的专业研发平台。其基于DDD、分层设计、CQRS等经典研发理论从数据库到API全部覆盖可显著提升开发设计和编程效率帮助开发团队实现更高的质量和生产力
- **1.2 核心价值/目标用户:** 从软件工程理论出发,提供软件设计能力,设计结果可直接转换为标准格式的代码,提升编码一致性及效率
- **1.3 主要特性概览:** 可视化设计、模型关联、多人协作、代码生成器等
### **2. TOCO 设计元素:**
#### **2.1 模块 (Module)**
- **定义与用途:** 在TOCO中我们将系统领域细分为具体的模块映射为Java工程中的module。这些模块代表了系统的叶子子域每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性并能提高开发效率和代码质量
- **关键配置:** 名称(小写英文字母+下划线如meeting,user_detail,禁止加任何固定后缀,全局唯一),描述
- **与其他元素关系:** 下面的每种设计元素都属于一个模块
- **代码产物:** 每个Module会单独生成一个Java Module项目路径/modules/模块名内部采用了entrance、service、manager、persist、common分层结构
#### **2.2 枚举 (Enum)**
- **定义与用途:** Enum用来表达一些常量值的集合可被其他模块使用可被用来做为字段的类型
- **关键属性/配置:** 名称(以_enum结尾,全局唯一),枚举值列表(全大写字母+下划线)
- **与其他元素关系:** 枚举可以作为其他对象Entity、Dto、Vo、Bto、Qto、Eo)的字段类型使用。
- **Enum设计元素的表达:**
- 以Json格式表达json schema 定义如下
```json
{
"type": "object",
"properties": {
"name": { "type": "string","description": "名称,使用英语表达单词之间使用下划线分割总长度不能超过32个字符"},
"uuid": { "type": "string","description": "计元素Enum)在TOCO中的唯一标识符在创建枚举的时候该字段为空; 在更新时,该字段不能为空"},
"description": { "type": "string", "description": "描述这个枚举的具体含义介绍这个枚举的用途控制在128个字符以内"},
"moduleName": { "type": "string", "description": "指定该设计元素(Enum)所属的模块,在创建的时候该字段不能为空,在更新的时候,该字段可以为空"},
"values": {
"type": "array","description": "枚举值列表",
"items": {"type": "string","description": "枚举值,使用英语表达, 单词之间使用下划线分割总长度不能超过32个字符"}
}
},
"required":["name","description"]
}
```
* **代码产物和修改建议**
* **生成产物**在common模块中生成一个Java类
* **职责:** 表达Enum的数据结构
* **命名规则**类名以Enum结尾
* **类路径:** 位于 ```**.common.enums``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Enum在TOCO中的uuid}|ENUM|DEFINITION
- **生成代码:** Enum会在common层生成Enum文件如StatusEnum
- **修改建议:** 不建议修改
#### **2.3 值对象 (Eo)**
- **定义与用途:** EO为一种POJO对象结构可被其他模块使用可被用来做为实体字段的类型。
- **关键属性/配置:** 名称(以_eo结尾,全局唯一)。EO的字段类型只能为基本类型含List、EO、Enum其他类型不允许。
- **与其他元素关系:** 可以作为其他对象Entity、Dto、Vo、Bto、Qto)的字段类型使用同时一个Eo中可以嵌套其他EO作为字段类型。
- **Eo设计元素的表达:**
- 以Json格式表达Json Schema定义及如下
```json
{
"type":"object",
"properties": {
"name":{ "type": "string", "description": "名称,使用英语表达单词之间使用下划线分割总长度不能超过32个字符"},
"description": { "type": "string","description": "描述,描述这个数据结构的具体含义介绍这个数据结构的用途控制在128个字符以内"},
"uuid":{ "type": "string", "description": "该设计在TOCO中的唯一标识符在创建EO的时候该字段为空; 在更新时,该字段不能为空"},
"moduleName":{ "type": "string", "description": "指定该设计元素所属的模块,在创建的时候该字段不能为空,在更新的时候,该字段可以为空"},
"fieldList":{
"type":"array", "description": "定义Eo的属性字段列表",
"items":{
"type": "object",
"properties":{
"name": { "type": "string","description": "属性字段名称,使用英语表达单词之间使用下划线分割总长度不能超过32个字符"},
"uuid":{ "type": "string","description": "当字段类型是枚举、值对象的时候,该字段不能为空,表示该字段对应的枚举或者值对象" } ,
"type":{ "type": "string","description": "字段类型可以是String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum, BigDecimal,List" },
"innerUuid":{"type": "string", "description": "当innerType是Eo或者Enum的时候表示字段类型对应的枚举或者值对象"},
"innerType": { "type": "string", "description": "当type是List的时候表示List包含的元素类型, 类型可以是String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum, BigDecimal"}
},
"required":[ "name","type"]
}
}
},
"required":["name","description"]
}
```
* **代码产物和修改建议**
* **生成产物**在persist层生成结构定义类文如AddressEo
* **职责:** 表达POJO数据结构
* **命名规则**类名以Eo结尾
* **类路径:** 位于 ```**.persist.eo``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Eo在TOCO中的uuid}|EO|DEFINITION
- **修改建议:** 不建议修改
#### **2.4 实体关系 (ER / Entity)**
- **定义与用途:** 实体及其关系。一个实体一般对应一个数据库表,关系为实体间的外键依赖关系
- **关键属性/配置:** 实体中包含名称、字段、字段类型、主键、索引等关系分为1:1和1:N关系
- **与其他元素关系:** 实体关系是聚合的基础也是DTO和VO的派生基础
- **代码产物和修改建议**
- 结构定义
* **生成产物**Java类按照Mybatis-plus的要求生成
* **职责:** 按照Mybatis-plus的要求生成结构定义类文件
* **类路径:** 位于 ```**.persist.dos``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|DEFINITION
- Mapper
* **生成产物**在persist层生成Mybatis-plus的Mapper类
* **职责:** 提供Mapper给Mybatis-plus框架
* **命名规则**类名以Mapper结尾(${entityName}Mapper)
* **类路径:** 位于 ```**.persist.mapper.mybatis``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Eo在TOCO中的uuid}
- Dao接口
* **生成产物**在persist层生成Dao接口
* **职责:** 提供Entity数据的查询接口为service层提供数据访问入口
* **命名规则**类名以Dao结尾(${entityName}Dao)
* **类路径:** 位于 ```**.persist.mapper``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|IDAO
- Dao实现
* **生成产物**在persist层生成Dao接口的实现类文件
* **职责:** 通过调用Mapper实现实现Dao接口
* **命名规则**类名以DaoImpl结尾(${entityName}DaoImpl)
* **类路径:** 位于 ```**.persist.mapper``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|ENTITY|DAO
- **修改建议:** 不建议修改
#### **2.5 聚合对象 (BO/业务对象)**
- **定义与用途:** 在TOCO中聚合对象是对一组密切关联的实体的封装。聚合对象从单一实体开始这个实体我们称为聚合根通过实体间关系不断的顺序将其他实体按层级关系组装进这个聚合对象。聚合对象可以按实体表达为树形结构。聚合对象提供了这组实体的内存一致性视图提供数据操作入口。由于写操作的内聚性聚合对象只能在单一模块中组合而且一个实体只能属于一个对象。同样如果有实体不在任何一个聚合对象中TOCO将无法提供与之相关的写方法。
- **包含元素:** 聚合对象包括聚合根及其聚合下的其他子实体对象例如商品聚合ProductBO中商品基本信息实体是ProductBO的聚合根商品SKU实体、商品库存实体是ProductBO的子聚合对象。
- **关键配置:** 名称(${EntityName驼峰}BO结尾如StaffBO),聚合根实体,聚合子对象实体。每个聚合必须包含一个聚合根
- **与其他元素关系:** 聚合是写方案的基础
- **代码产物和修改建议**
- 综述
- 业务对象包含多个Entity通过业务对象的嵌套组合表达了Entity之间的关系如果一个业务对象包含了子对象则会生成BO和BaseBO,BaseBO封装实体属性和关系子类留给业务扩展逻辑
如果是叶子节点不存在子对象的BO则直接生成BO类文件不生成BaseBO类文件
- BO
* **生成产物**在Manager层生成聚合对象类文件符合Hibernate的标准
* **职责:** 定义聚合对象,多个聚合对象组合成层级结构实现充血模型,支持写链路上的数据变更,监听数据变更,支持数据校验
* **命名规则**类名以BO结尾(${entityName}BO)
* **类路径:** 位于 ```**.manager.bo``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|BO|DEFINITION
- **修改建议:** 建议修改BO中的validateAggregate或valid方法。不建议修改检验方法以外的其他代码如果发现需求中有业务不变性校验**注意** 上述的校验方法,在写方案内部由框架触发调用,而不是业务代码显式调用
- BaseBO
* **生成产物**对于存在子BO的聚合对象封装不变的代码部分
* **职责:** 定义聚合对象,多个聚合对象组合成层级结构实现充血模型,支持写链路上的数据变更,监听数据变更,支持数据校验
* **命名规则**类名以BO结尾(${entityName}BO)
* **类路径:** 位于 ```**.manager.bo``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定 ,uuid规则: ${Entity在TOCO中的uuid}|BO|DEFINITION
- **修改建议:** 建议修改BO中的validateAggregate或valid方法如果发现需求中有业务不变性校验;不建议修改检验方法以外的其他代码
#### **2.6 数据传输对象 (DTO)**
- **定义与用途:** 在TOCO中DTO表达某个Entity为基本通过外键关系不断关联多个Entity的数据结构。DTO还隐式表达了数据的取数拼装这种拼装符合外键关系。DTO分为BaseDTO和普通DTOBaseDTO派生自Entity包含Entity的所有字段每个Entity有且仅有一个BaseDTO普通DTO派生自BaseDTO包含BaseDTO的所有字段且可以增加扩展字段或自定义字段。注意DTO不能作为接口的参数也不能作为HTTP API的返回值
- **如何创建/生成:** 对于每个EntityTOCO会自动生成一个BaseDTO命名为${Entity名字}BaseDto如UserBaseDto该BaseDTO包含了Entity的全部字段。除了BaseDTO其他的DTO均需要手动以BaseDTO为根来创建。在TOCO中必须要先判断需要的DTO是否为BaseDTO如果是BaseDTO则可通过Entity名称获取BaseDTO如果不是BaseDTO则需要通过DTO要表达的信息来创建DTO会议及其议程信息。
- **关键配置:** 名称(BaseDTO以BaseDto结尾其他DTO以Dto结尾,全局唯一)、根Entity、字段列表。DTO中的字段分为三种a.继承Entity或BaseDTO的字段和Entity及BaseDTO的字段类型一样b.扩展字段含正向替换和反向注入字段类型为DTO或List<DTO>;c.自定义字段类型为基本类型、Eo、Enum、DTO类型。BaseDTO中一般包含Entity的全部字段,DTO中一般包含BaseDTO中的全部字段不进行字段裁剪可以根据外键关系扩展其他Entity(详见**字段扩展方式**)在明确无法扩展外部Entity的情况下可增加对应的自定义字段。
- **字段扩展方式:**TOCO定义了一个DTO组装方法适用于DTO通过外键关系替换/注入对应Entity的信息对象化表达有外键关系的Entity信息。只要存在外键关系且满足以下条件即可扩展a.对于正向替换当前实体存在指向其他实体的外键字段b.对于反向注入:其他实体存在指向当前实体的外键字段。
例如有两个Entity
```
MeetingRoom{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
Meeting { //会议
Long id;// 会议id,主键
Long roomid; //占用的会议室id到MeetingRoom的外键n:1关系
Long backupRoomid; //备用的会议室id到MeetingRoom的外键n:1关系
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
```
其中Meeting和MeetingRoom是n:1关系。即多个会议室会占用同一个会议室。
当组装对象以某Entity为根时那么首先它将拥有和该Entity一样的数据结构并将通过下面的“正向替换”“反向注入”的行为递归的将多个互相之间有外键关系的Entity的信息组装到该组装对象中。
定义“正向替换”这个行为,选定一个表,这张表存在到另外一张表的一个或多个外键。选择和需求相关的具体的外键属性,将该外键属性替换为另一张表为根的组装对象。这样就可以获取基于某些外键且包含另一张表更详细的属性数据。
例如需要会议和其占用会议室时将Meeting表中的roomid外键替换为以MeetingRoom为根的组装对象而backupRoomid对应的候选会议室具体信息和本需求无关不做任何替换。如下
```
MeetingWithRoomDto {
Long id;// 会议id
MeetingRoomDto room { //正向替换该会议用的会议室信息,是一个以会议室为根的对象
Long id; //会议室ID
String name;// 会议室名称
}
Long backupRoomid; // 不做变化
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
```
又例如需要会议和其候选会议室时将Meeting表中的backupRoomid进行正向替换。
又例如需要会议且即需要其占用会议室也需要候选会议室时将Meeting表中的roomidbackupRoomid都进行正向替换。
同时TOCO还定义了“反向注入”这个行为选定一个表如果有另外的表到前表有外键选择和需求相关的具体的外键属性在选定表中增加一个以另外表为根的组合对象(当外键关系是1:1时)或者组合对象的列表(当外键关系是n:1时)。需求是“获取会议室和占用它的会议信息”需要选定MeetingRoom那么基于另外的表Meeting中存在字段roomid为到MeetingRoom的n:1关系外键。可以将List<MeetingBaseDto>反向注入到MeetingRoom中最终生成一个以Meeting为根的组装对象生成
```
MeetingRoomWithMeetingDto {
Long id;// 会议室id
String name;// 会议室名称
List<MeetingBaseDto> meetingList { //反向注入的用该会议室的会议信息,是以会议为根的对象
Long id;// 会议id
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
}
```
这种“正向替换”和“反向注入”可以按需递归调用去将多个互相之间有外键关系的对象组装成最终对象。例如还有另外一张表MeetingAgenda到Meeting有n:1的外键和另外一张表AgendaAttendance到MeetingAgenda有n:1外键。那么如果我要去组装以Meeting开始包含MeetingRoom, MeetingAgenda, AgendaAttendance的组装对象首先发现MeetingRoom是可以正向扩展到Meeting的反向注入MeetingAgenda而AgendaAttendance需要先反向注入到MeetingAgenda中。
- **TOCO中DTO的json结构描述:** 在TOCO中DTO使用一个json结构表示该结构可用于理解DTO的含义或作为创建、更新DTO工具的参数。部分字段的含义为dto的uuid为唯一标识如果需要创建DTO则设置为null如果需要复用则填入其uuid。expandList为正向替换reverseExpandList为反向注入customFieldList为自定义字段。expandListList中foreignKeyInThisEntity为正向替换对应的本表外键字段的名字dtoFieldName为正向替换之后给该字段的起的新名字reverseExpandList中foreignKeyInOtherEntity为反向注入对应的他表外键字段的名字dtoFieldName为反向注入之后给该字段的起的新名字customFieldList中uuid为自定义字段特有的UUID创建DTO的时候不需要填入因为TOCO会自动为其分配UUID更新DTO的时候需要传入用于定位需要更新的自定义字段typeUuid参数对应类结构的UUID当type为Enum、Eo时包含该字段innerType为List内部类型当type为List时包含该字段innerUuid为List内部类结构的UUID当type为List且innerType=Enum、Eo时包含该字段。示例如下
- meeting_with_room_dto
```json
{
"dto": {
"uuid": null,
"name": "meeting_with_room_dto",
"description": "会议详情,包含会议室信息,以及其中的会议列表",
"fromEntity": "meeting",
"expandList": [
{
"foreignKeyInThisEntity": "room_id",
"dtoFieldName": "meeting_room",
"dto": {
"uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1",
"name": "meeting_room_with_meetings_dto",
"fromEntity": "meeting_room",
"description": "会议室信息,包含会议列表"
}
}
],
"customFieldList":[
{
"uuid": "自定义字段的唯一标识更新DTO的时候需要传入",
"name": "status",
"type": "Enum",
"typeUuid": "对应Enum的uuid",
"description": "当前状态"
}
]
}
}
```
- meeting_room_with_meetings_dto
```json
{
"dto": {
"uuid": "d05c7b3d-1c92-45a1-2113-a01b245813c1",
"name": "meeting_room_with_meetings_dto",
"description": "会议室详情,包含会议室信息,以及其中的会议信息",
"fromEntity": "meeting",
"reverseExpandList": [
{
"foreignKeyInOtherEntity": "room_id",
"dto": {
"uuid": "ffeec02d-2a32-1531-1ce1-b9bfc1993765",
"name": "meeting_base_dto",
"description": "会议基本信息",
"fromEntity": "meeting"
},
"dtoFieldName": "meeting_list"
}
]
}
}
```
示例中meeting_with_room_dto没有uuid为待创建的DTO。meeting_base_dto和meeting_room_with_meetings_dto为已存在的DTO带有uuid。
- **预定义方法:** 对于每一个DTOTOCO会自动其根Entity的唯一索引生成一些预定义的RPC方法及实现预定义方法不仅获取了根Entity的数据还通过RPC调用的方式**自动**获取了**所有扩展字段**的数据并对数据进行了拼装处理。如实体user有unique index(username)则会针对UserDto生成UserDto UserDtoService.getByUserName(String userName)和List<UserDto> UserDtoService.getByUserNames(List<String> userNames)。预定义方法内部根据外键关系自动生成了复杂DTO数据的递归、Join拼装的能力会直接返回DTO内部的所有继承字段和扩展字段的数据。注意自定义字段的数据获取不会自动生成需要在对应的convert方法中编写代码。
- **公开性:** DTO可以设置公开性如果DTO为公开则其生成的预定义RPC方法也为公开RPC可以被其他模块订阅并调用如果DTO为非公开则其生成的预定义RPC方法也为非公开RPC其他模块不可见
- **跨模块依赖:** 如果DTO内引用了其他模块的DTO则需要订阅其他模块的RPC(getBy${PrimaryKey},getBy${PrimaryKey}s,getBy${foreignKey}等)方法用来获取对应的DTO
- **复杂嵌套DTO获取流程:** 如果我们需要获取DTO通常有2种方式且两种方式都可以直接获取到复杂嵌套的DTO数据
- 第1种: 通过预定义方法获取DTO
- 第2种: 通过读方案获取DTO
在TOCO中通常使用以下步骤判断使用哪种方式
- a. 如果查询DTO的条件为主键或唯一索引的值或列表则直接使用第1种方式
- b. 如果通过其他复杂查询条件则可以采用第2种方式
注意判断使用哪种方式时,只能根据查询条件判断是否使用读方案,绝对禁止使用返回值是否需要数据拼装来判断!**如果现实中的代码和TOCO中的步骤有冲突时只能使用TOCO的定义**
- **代码产物和修改建议**
- 结构定义
* **生成产物**一个Java类
* **职责:** 表达DTO的数据结构
* **命名规则**类名以Dto结尾
* **禁止** 修改该类
* **类路径:** 位于 ```**.manager.dto``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${DTO在TOCO中的uuid}|DTO|DEFINITION
- **Manager**
* **生成产物:** Java接口以及实现类
* **命名规则:** 接口类名以Manager结尾、实现类名以ManagerImpl结尾(${DtoName}Manager)、基类以名ManagerBaseImpl结尾(${DtoName}ManagerImpl)
* **职责:** 提供了DTO数据的获取的接口包括根据id单个、id列表批量获取、以及根据DTO对应的实体的数据库索引获取
* **类路径:** 位于 ```**.manager``` 包路径下
* **禁止** 删除该类中的任何系统自动生成的函数
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${DTO在TOCO中的uuid}|DTO|MANAGER
- **Converter**
* **生成产物:** Java实现类以及基类
* **禁止** 删除该类中的任何系统自动生成的函数
* **命名规则:** 实现类名以Converter结尾(${DtoName}Converter)、基类名以BaseConverter结尾(${DtoName}BaseConverter)
* **职责:** Entity转换到BaseDTO或则BaseDTO转化为普通DTO从Entity转为BaseDTO的方法命名为convert${EntityName}To${DtoName}从BaseDTO转换为DTO的方法命名为convert${BaseDtoName}To${DtoName}
* **类路径:** 位于 ```**.manager.converter``` 包路径下
* **唯一标识符位置:**
* 实现类Converter 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${DTO在TOCO中的uuid}|DTO|CONVERTER
* 基类BaseConverter 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
- **例子:**
* 如UserDto、UserDtoManager、UserDtoConverter extends UserDtoBaseConverter、UserDtoService或名称为${DtoName}Service内部包含getBy${PrimaryKey},getBy${PrimaryKey}s等方法。如果Dto为UserBaseDto则生成的类名为UserBaseDtoService
- **修改建议:**
- 建议在Service与BaseConverter中进行代码扩展不建议修改结构定义文件和Manager文件。其中DTO的**自定义字段**由于不直接派生自Entity所以一般会对应取数逻辑代码。通常如果涉及到数据获取、计算和拼装批量处理的性能最好所以代码位置**必须**放在BaseConverter中已经自动生成的**列表**转换方法中批量取数组装如UserBaseDtoBaseConverter.convertUserToUserBaseDto(List<User>)或UserDtoBaseConverter.convertUserBaseDtoToUserDto(List<UserBaseDto>)
#### **2.7 视图对象 (VO)**
- **定义与用途:** 在TOCO中VO表达某个BaseDTO(如果用户指明派生源也可使用其他DTO)为派生源通过外键关系不断关联多个BaseDTO的数据结构。VO用于在视图层与前端之间进行数据传输往往被当做HTTP API的返回值、或读方案的返回值使用由服务端返回至前端。注意VO不能作为接口的参数也不能作为RPC的返回值。
- **关键配置:** 名称(以Vo结尾,全局唯一)、根Entity、派生源、字段列表。VO中的字段分为三种a.继承DTO的字段如果DTO中的字段为基础类型或EO、Enum则VO中的继承字段和和DTO的字段类型一样如果DTO中的字段为DTO或List<DTO>类型则继承字段为字段对应的DTO类型派生出的VO或List<VO>类型b.扩展字段含正向替换和反向注入字段类型为VO或List<VO>;c.自定义字段类型为基本类型或VO类型。VO中的字段来源于DTO可以根据页面需要将无用字段进行裁剪可以根据外键关系扩展其他BaseDto(详见**字段扩展方式**)。如果在派生源中没有合适字段且明确无法通过外键扩展外部BaseDto的情况下可增加对应的自定义字段
- **继承字段类型转换:** VO只和其派生源VO之间有转换关系。当一个VO派生自某个DTO且该VO继承了派生源DTO中的DTO类型字段(假设为DTO-A)此时VO中对应的继承字段类型必须为VO(假设为VO-A),且**VO-A也必须派生自DTO-A**如此才能维持VO和DTO之间的转换关系
- **与DTO的区别 (在TOCO语境下): **DTO用于服务层传输通常作为RPC的返回值与数据模型更近复用性较强VO用于视图层传输通常作为API的返回值与UI展示更为接近可裁剪掉不需要的DTO中的冗余字段复用性较弱。
- **如何创建/生成:** VO通常由某个BaseDTO以及外键关系为基础派生也可以直接创建和DTO无关、内部全为自定义字段的VO尽量少用只为应对某些特殊页面需要组装一组完全无关的返回数据的场景
- **根VO和子VO:**TOCO中的VO分为两种1.根VO指最外层的VO结构需要经由TOCO创建有uuid作为唯一标识根VO可被其他根VO或子VO引用2.子VO某个根VO的内部嵌套VO通过外键关系关联BaseDTO之后由TOCO自动创建只附属于某个根VO只能被这一个根VO引用且没有uuid。所以在TOCO中我们需要描述VO要的字段、扩展关系并通过**创建根VO**的行为使TOCO**自动创建**其子VO或引用其他根VO即可完成一个复杂嵌套VO的创建过程无需单独创建子VO。
- **字段扩展方式:**同DTO的字段扩展方式可以通过任意**一个**派生源BaseDTO即可经过外键扩展成为复杂嵌套VO。只要存在外键关系且满足以下条件即可扩展对于正向替换当前实体存在指向其他实体的外键字段对于反向注入其他实体存在指向当前实体的外键字段。
例如有两个BaseDTO
```
MeetingRoomBaseDto{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
MeetingBaseDto { //会议
Long id;// 会议id,主键
Long roomid; //占用的会议室id到MeetingRoom的外键n:1关系
Long backupRoomid; //备用的会议室id到MeetingRoom的外键n:1关系
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
```
其中Meeting和MeetingRoom是n:1关系。即多个会议室会占用同一个会议室。
通过“正向替换”这个行为可以组装出如下VO
```
MeetingWithRoomVo { //根VO需要通过TOCO创建
Long id;// 会议id
MeetingRoomVo room { //正向替换该会议用的会议室信息TOCO自动生成的内部VO派生自MeetingRoomBaseDto
Long id; //会议室ID
String name;// 会议室名称
}
Long backupRoomid; // 不做变化
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
```
通过“反向注入”这个行为,可以生成:
```
MeetingRoomWithMeetingVo{
Long id;// 会议室id
String name;// 会议室名称
List<MeetingVo> meetingList{ //反向注入的用该会议室的会议信息TOCO自动生成的内部VO派生自MeetingBaseDto
Long id;// 会议id
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
}
```
- **TOCO中VO的json结构描述:** 在TOCO中VO使用一个json结构表示该结构可用于理解VO的含义或作为创建、更新VO工具的参数。部分字段的含义为expandList为正向替换reverseExpandList为反向注入extendFieldList为来自派生源DTO的字段customFieldList为自定义字段。expandListList中foreignKeyInThisEntity为正向替换对应的本表外键字段的名字voFieldName为正向替换之后给该字段的起的新名字reverseExpandList中foreignKeyInOtherEntity为反向注入对应的他表外键字段的名字voFieldName为反向注入之后给该字段的起的新名字customFieldList中uuid为自定义字段特有的UUID创建DTO的时候不需要填入因为TOCO会自动为其分配UUID更新DTO的时候需要传入用于定位需要更新的自定义字段typeUuid参数对应类结构的UUID当type为List且innerType=Enum、Eo时会包含该字段extendFieldList中name为继承字段的名称如果要继承的DTO字段不是DTO类型则VO中的字段类型将与DTO字段一致如果DTO中的字段为DTO或List<DTO>类型由于VO中的字段类型不能为DTO**必须**将该DTO转换为VO才可作为VO字段使用所以extendFieldList中会有**一个vo结构**用来表示该字段DTO类型派生出的VO定义注意该VO字段中的VO类型必须派生自继承的DTO字段类型
示例如下:
系统中存在meeting_detail_dto
```json
{
"uuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"name": "meeting_detail_dto",
"fromEntity": "meeting",
"description": "会议详情",
"expandList": [
{
"foreignKeyInThisEntity": "create_user_id",
"voFieldName": "create_user",
"dto": {
"uuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"name": "user_base_dto",
"fromEntity": "user",
"description": "用户基本信息"
}
}
]
}
```
- meeting_with_room_vo由meeting_detail_dto派生并继承了id和create_user字段
```json
{
"vo": {
"uuid": null,
"name": "metting_with_room_vo",
"description": "会议详情,包含会议室信息,以及会议室禁用列表",
"rootVo": "metting_with_room_vo",
"fromEntity": "meeting",
"fromDto": "meeting_detail_dto",
"fromDtoUuid": "cd55c96b-aa67-bfb2-7614-70b503a8f8bf",
"extendFieldList":[
{
"name": "id"
},
{
"name": "create_user",
"vo": {
"uuid": null,
"name": "user_base_vo",
"description": "用户信息",
"rootVo": "metting_with_room_vo",
"fromEntity": "user",
"fromDto": "user_base_dto",
"fromDtoUuid": "53bb59cf-1ed2-6fb3-9f61-895b638903d8",
"reverseExpandList": [],
"customFieldList":[],
"extendFieldList":[
{
"name": "name"
},
{
"name": "gender"
}
]
}
}
],
"expandList": [
{
"foreignKeyInThisEntity": "room_id",
"voFieldName": "meeting_room",
"vo": {
"name": "meeting_room_with_meetings_vo",
"description": "会议室信息,包含会议列表",
"rootVo": "metting_with_room_vo",
"fromEntity": "meeting_room",
"fromDto": "meeting_room_base_dto",
"fromDtoUuid": "88437212-6370-99a6-1e7a-fe1469082d08",
"reverseExpandList": [
{
"foreignKeyInOtherEntity": "room_id",
"vo": {
"uuid": null,
"name": "meeting_base_vo",
"description": "会议基本信息",
"rootVo": "metting_with_room_vo",
"fromEntity": "meeting",
"fromDto": "meeting_base_dto",
"fromDtoUuid": "1a768c5e-b449-db5d-fe55-9d572d64332a",
"extendFieldList":[
{
"name": "startTime"
},
{
"name": "endTime"
}
]
},
"voFieldName": "meeting_list"
}
],
"customFieldList":[
{
"uuid": "自定义字段的唯一标识更新DTO的时候需要传入",
"name": "occupied",
"type": "Boolean",
"description": "是否被占用"
},
{
"uuid": "自定义字段的唯一标识更新DTO的时候需要传入",
"name": "custom_eo",
"type": "Eo",
"typeUuid": "uuid of an eo"
},
{
"uuid": "自定义字段的唯一标识更新DTO的时候需要传入",
"name": "status_list",
"type": "List",
"innerType": "Enum",
"innerUuid": "uuid of an enum"
},
{
"uuid": "自定义字段的唯一标识更新DTO的时候需要传入",
"name": "custom_string_list",
"type": "List",
"innerType": "String"
}
],
"extendFieldList":[
{
"name": "id"
},
{
"name": "name"
}
]
}
}
]
}
}
```
示例中meeting_with_room_vo为根VO但没有uuid为待创建的根VO。meeting_room_with_meetings_vo和meeting_base_vo为meeting_with_room_vo的子VO无法被其他根VO引用且没有uuid。另外特别注意metting_with_room_vo的extendFieldList中create_user结构含有一个vo是因为在派生源meeting_detail_dto中create_user字段为user_base_dto类型所以在metting_with_room_vo中该继承字段类型需要变为由user_base_dto派生出的VO结构注意metting_with_room_vo中的create_user对应的user_base_vo**必须派生自**DTO字段的user_base_dto类型。
- **派生源默认使用BaseDTO:** 除非用户指定了VO的派生源DTO否则创建VO时只可以用**BaseDTO**为派生源。
- **与DTO的转换关系:** 在创建一个**有派生源的**VO后TOCO会在生成代码时自动生成2种convert方法1.基础convert方法从DTO转换为VO仅转换结构以及基本类型字段的get/set方法命名为convertTo${VoName}、convertTo${VoName}List、convertTo${VoName}Map其中**Map转换方法**为底层批量方法通常也是自定义字段逻辑编写的位置复用性好会被其他convert方法调用单个和列表convert方法都通过**调用Map方法**来实现;2.带数据拼装逻辑的convert方法内部会**自动**调用基础convert方法从DTO转换为VO并设置基本类型字段数据然后再根据外键**自动**获取**扩展字段**的数据以拼装最终数据方法命名为convertAndAssembleData、convertAndAssembleDataList也就是说这两个方法已经**自动**获取了所有**继承字段**和**扩展字段**的数据)。这2种方法对应的代码会生成在VO对应的Converter类中
- **字段数据获取:** 对于继承自DTO的字段、以及扩展字段TOCO会在convert方法中自动生成数据的获取代码无需手动实现这两种字段的获取和拼装逻辑。对于自定义字段则**必须**在最底层的convertTo${VoName}Map方法中实现对应的获取和拼装逻辑以便于其他convert方法都能够**复用**这段逻辑。**禁止**在其他convert方法中实现自定义字段逻辑因为这样会导致某些场景下数据拼装不完整。
- **跨模块依赖:** 如果VO内存在由其他模块DTO派生出的子VO则需要订阅其他模块的RPC(getBy${PrimaryKey},getBy${PrimaryKey}s,getBy${ForeignKey}s等)方法用来获取对应的DTO然后再转换为子VO
- **复杂嵌套VO获取流程:** 如果我们需要获取VO通常有3种方式且3种方式都可以直接获取到复杂嵌套的VO数据
- 第1种: 先通过预定义方法获取VO的派生源BaseDTO再通过convertAndAssembleData或convertAndAssembleDataList方法转换成VO
- 第2种: 先通过读方案获取VO的派生源BaseDTO再通过convertAndAssembleData或convertAndAssembleDataList方法转换成VO
- 第3种: 通过读方案直接获取VO
在TOCO中通常使用以下步骤判断使用哪种方式
- a. 如果查询VO的条件为主键或唯一索引的值或列表则直接使用第1种方式
- b. 如果通过其他复杂查询条件则可以采用第2种或第3种方式
- c. 如果用户上下文中有指定的返回DTO的读方案符合条件则使用第2种否则使用第3种
注意判断使用哪种方式时,只能根据查询条件判断是否使用读方案,绝对禁止使用返回值是否需要数据拼装来判断!**如果现实中的代码编写方式和TOCO中的步骤有冲突时只能使用TOCO的定义**
- **代码产物和修改建议**
- **结构定义**
* **生成产物:** 在controller层生成一个Java类
* **命名规则:** 类名以Vo结尾
* **职责:** 表达VO的数据结构
* **类路径:** 位于 ```**.entrance.web.vo``` 包路径下
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${VO在TOCO中的uuid}|VO|DEFINITION
- **Converter**
* **生成产物:** 在controller层生成一个Java类(**有派生源**的VO才有Converter)和基类
* **命名规则:** 实现类名以Converter结尾(${VoName}Converter)基类名以BaseConverter结尾(${VoName}BaseConverter)
* **类路径:** 位于 ```**.entrance.web.converter``` 包路径下
* **职责:** 把DTO转换成VOConverter中包含2种convert方法1.基础convert方法从DTO转换为VO仅转换结构方法命名为convertTo${VoName}、convertTo${VoName}List、convertTo${VoName}Map其中**Map转换方法**为底层批量方法单个和列表convert方法都通过**调用Map方法**来实现;2.带数据拼装逻辑的convert方法内部会调用基础convert方法从DTO转换为VO然后再根据外键获取拼装最终数据方法命名为convertAndAssembleData、convertAndAssembleDataList)
* **唯一标识符位置:**
* 实现类Converter其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${VO在TOCO中的uuid}|VO|CONVERTER
* 基类BaseConverter其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${DTO在TOCO中的uuid}|DTO|BASE_CONVERTER
* **例子:**
* 如UserDetailVo、UserDetailVoConverter包含convertToUserDetailVo、convertToUserDetailVoList、convertToUserDetailVoMap、convertAndAssembleData、convertAndAssembleDataList方法
- **修改建议:**
- 建议在Converter中进行代码扩展不建议修改结构定义文件。其中VO的**自定义字段**由于不直接派生自DTO所以一般会对应取数逻辑代码。通常如果涉及到数据获取、计算和拼装批量处理的性能最好所以自定义字段对应的代码位置**必须**放在Converter的**Map**基础转换方法convertTo${VoName}Map中批量取数组装如UserVoConverter.convertToUserVoMap
#### **2.8 查询对象(WO)**
- **定义与用途:** 在TOCO中WO表达某个Entity为基本通过外键关系不断关联多个Entity的数据结构。WO还隐式表达了数据的取数拼装这种拼装符合外键关系. WO作为ReadPlan的查询上下文使用所以在创建ReadPlan之前需要先创建WO对象。在理解一个ReadPlan的语义的时候需要以WO作为上下文。
如果对应的需求在返回DTO|VO的时候需要对DTO|VO的列表属性进行过滤,则需要根据对应的DTO|VO的结构定义扩展出对应的WO对象需要过滤的字段名需要和DTO|VO的字段名保持一致),例如:
查询DTO
```java
class MeetingDto {
String meetingId;
String meetingName;
List<MeetingAgenda> agendaList;
}
```
需求是需要根据会议名称查询会议列表,并且根据会议议程信息过滤掉部分议程,那在定义查询对象的时候就需要包含议程信息,并且扩展字段的名称要定义为**agendaList**
- **查询对象设计元素的表达**
- 以json格式表达json schema 定义如下
```json
{
"type": "object","description": "查询对象定义", "required": ["dtoOrVoId","name","fromEntity"],
"properties": {
"name": {"type": "string", "description": "查询对象名称用英语表达单词之间下划线分割长度补超过32个字符"},
"uuid": {"type": "string", "description": "查询对象uuid, 在更新的时候必须传递(只有根节点必须传递),创建的时候不传递"},
"dtoOrVoId":{"type":"string","description":"返回数据对象(VO或DTO)的uuid,创建的时候必须指定,更新的时候不传递"},
"moduleName": {"type": "string", "description": "查询对象所属模块名称,在创建的时候必须传递"},
"fromEntity": {"type": "string", "description": "查询对象对应的实体"},
"expandList": {
"type": "array", "description": "正向扩展列表",
"items":
{
"type": "object","description": "正向扩展定义","required": ["field","wo","fieldName"],
"properties": {
"field": {"type": "string", "description": "指定fromEntity的扩展字段必须是fromEntity的外键字段"},
"wo": {"$ref": "#"},
"fieldName": {"type": "string", "description": "扩展出来的字段名称"}
}
}
},
"reverseExpandList": {
"type": "array", "description": "反向扩展列表",
"items":
{
"type": "object","description": "反向扩展定义","required": ["field","wo","fieldName"],
"properties": {
"field": {"type": "string", "description": "指定fromEntity的扩展字段必须是fromEntity的外键字段"},
"wo": {"$ref": "#"},
"fieldName": {"type": "string", "description": "扩展出来的字段名称"}
}
}
}
}
}
```
- **如何创建/生成:**
- **创建思路** 按照需要查询返回的DTO|VO的结构构建出同构的WO对象扩展和反向扩展的字段名保持一致然后根据查询需求和过滤需求对WO进行二次裁剪或和扩展
- 去掉过滤和查询都不需要的扩展
- 补全查询或者字段过滤需要扩展
- **关键配置:** Wo中的字段分为三种a.继承Entity的字段和Entity的字段类型一样b.扩展字段含正向替换和反向注入字段类型为WO或List<WO>;
- **字段扩展方式:** TOCO定义了一个WO组装方法适用于WO通过外键关系替换/注入对应Entity的信息对象化表达有外键关系的Entity信息。只要存在外键关系且满足以下条件即可扩展a.对于正向替换当前实体存在指向其他实体的外键字段b.对于反向注入:其他实体存在指向当前实体的外键字段。
例如有两个Entity
```
MeetingRoom{ //会议室
Long id;// 会议室id,主键
String name;// 会议室名称
}
Meeting { //会议
Long id;// 会议id,主键
Long roomid; //占用的会议室id到MeetingRoom的外键n:1关系
Long backupRoomid; //备用的会议室id到MeetingRoom的外键n:1关系
String title; //会议标题
DateTime startTime; //会议开始时间
DateTime endTime; //会议结束时间
}
```
其中Meeting和MeetingRoom是n:1关系。即多个会议室会占用同一个会议室。
当组装对象以某Entity为根时那么首先它将拥有和该Entity一样的数据结构并将通过下面的“正向替换”“反向注入”的行为递归的将多个互相之间有外键关系的Entity的信息组装到该组装对象中。
定义“正向替换”这个行为,选定一个表,这张表存在到另外一张表的一个或多个外键。选择和需求相关的具体的外键属性,将该外键属性替换为另一张表为根的组装对象。这样就可以获取基于某些外键且包含另一张表更详细的属性数据。
例如需要会议和其占用会议室时将Meeting表中的roomid外键替换为以MeetingRoom为根的组装对象而backupRoomid对应的候选会议室具体信息和本需求无关不做任何替换。如下
```
MeetingWithRoomWo {
Long id;// 会议id
MeetingRoomWo room { //正向替换该会议用的会议室信息,是一个以会议室为根的对象
Long id; //会议室ID
String name;// 会议室名称
}
Long backupRoomid; // 不做变化
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
```
又例如需要会议和其候选会议室时将Meeting表中的backupRoomid进行正向替换。
又例如需要会议且即需要其占用会议室也需要候选会议室时将Meeting表中的roomidbackupRoomid都进行正向替换。
同时TOCO还定义了“反向注入”这个行为选定一个表如果有另外的表到前表有外键选择和需求相关的具体的外键属性在选定表中增加一个以另外表为根的组合对象(当外键关系是1:1时)或者组合对象的列表(当外键关系是n:1时)。需求是“获取会议室和占用它的会议信息”需要选定MeetingRoom那么基于另外的表Meeting中存在字段roomid为到MeetingRoom的n:1关系外键。可以将List<MeetingBaseDto>反向注入到MeetingRoom中最终生成一个以Meeting为根的组装对象生成
```
MeetingRoomWithMeetingWo {
Long id;// 会议室id
String name;// 会议室名称
List<MeetingBaseWo> meetingList { //反向注入的用该会议室的会议信息,是以会议为根的对象
Long id;// 会议id
String title; //会议标题
DateTime startTime //会议开始时间
DateTime endTime //会议结束时间
}
}
```
这种“正向替换”和“反向注入”可以按需递归调用去将多个互相之间有外键关系的对象组装成最终对象。例如还有另外一张表MeetingAgenda到Meeting有n:1的外键,那么如果我要去组装以Meeting开始包含MeetingRoom, MeetingAgenda的查询对象首先发现MeetingRoom是可以正向扩展到Meeting的并且可以反向注入MeetingAgenda
- meeting_with_room_and_agenda_wo
```json
{
"wo": {
"uuid": null,
"name": "meeting_with_room_and_agenda_wo",
"dtoOrVoId":"96eedc80-2c29-44ab-883f-031efdba43e8",
"description": "会议详情,包含会议室信息,以及其中的会议列表",
"fromEntity": "meeting",
"expandList": [
{
"field": "room_id",
"fieldName": "meeting_room",
"wo": {
"name": "meeting_room_wo",
"fromEntity": "meeting_room",
"description": "会议室信息"
}
}
],
"reverseExpandList": [
{
"field":"meeting_id",
"filedName": "meeting_agenda_list",
"wo": {
"fromEntity": "meeting_agenda",
"name": "meeting_agenda_wo",
"description": "会议议程信息"
}
}
]
}
}
```
#### **2.9 读方案 (ReadPlan)**
- **定义与用途:** 在TOCO中针对DTO和VO读方案描述了如何基于查询对象从数据库获取DTO和VO列表数据主要提供了两个能力
- 根据查询条件返回符合条件的DTO或VO的id列表
- 根据字段过滤条件对DTO和VO的列表字段数据进行过滤
- **注意** 不派生自DTO的VO不能创建读方案
- 读方案能力边界
- 读方案的的查询能力是sql的简化实现了部分sql的查询能力仅支持Sql的 count、exists、left join, not、in、like、between、and、or、not 以及 ( ) , 以及排序 order by, 以及分页 limit offset 不能使用group by having 语法,不支持使用函数
- 在使用exists 和 left join的时候只能通过外键扩展其中如果外键是1N的时候只能使用exist 如果是11的时候则可以使用 left join 和 exists.
例如: 实体t 和他t1 t1存在列t_id为指向t的外键如果外键关系为1N则 select * from t where exists (select * from t1 where t.id=t1.id) 合法; elect * from t left join t1 on t.id=t1.id 非法
- 在过滤字段的选择上在使用exists的时候在条件中只能选择后表的字段 在使用 left join的时候则可以使用前表和后表字段
例如: 实体 t (id,name) ,实体 t1 (id,name,t_id) t1存在列t_id为指向t的外键, select * from t where exists (select * from t1 where t.id=t1.id and t.name like ) 非法, select * from t left join t1 on t.id=t1.id and t1.name like 合法
select * from t left join t1 on t.id=t1.id where t.name like 合法, select * from t left join t1 on t.id=t1.id where t1.name like 合法
- 在选择使用度方案的时候**必须**符合度方案能力编辑,如果需求在读方案的能力之外,必须使用 **2.16 自定义查询** 实现功能
- **特别注意** 对于根据主键获取单个或者批量获取DTO或者VO并且不需要对DTO、VO的列表字段数据进行过滤的需求必须使用**DTO自动生成的预定义方法**,禁止使用读方案
- **关键配置:** 名称(小写字母+下划线不要以read_plan结尾,全局唯一)、返回结构(DTO/VO一个读方案**不能**同时返回多种DTO或VO)、查询条件的自然语言描述、是否生成计数方法、排序字段(如果选择不分页,则不需要)、过滤字段以及过滤条件。
- **与RPC、代码的关系:** 对于每一个返回DTO的读方案TOCO会为每种分页方式自动生成一个RPC方法其参数为对应的QTO返回值为DTO列表如果选择了生成计数方法则还会在生成一个RPC参数为QTO返回值为符合条件的DTO数量。同样对于每一个返回VO的读方案TOCO会自动生成一个Java方法其参数为对应的QTO返回值为VO列表方法内部逻辑已经由TOCO完全实现;如果选择了生成计数方法则还会生成一个count方法参数为QTO返回值为符合条件的DTO数量方法内部逻辑已经由TOCO完全实现
- **生成的读方案RPC** 的使用: 读方案生成的RPC属于该写方案所属的模块其他模块如果需要调用该读方案需要先订阅该写方案RPC在生成代码后使用adapter调用该读服务当前模块则可以直接调用该读方案对应的Service
- **如何创建/生成:** 在创建读方案时,必须先调用工具创建或选择现有的**一种**DTO或VO作为返回值类型然后再定义查询条件的自然语言描述根据用户姓名、年龄、学校名称查询用户列表分页方式、是否生成计数方法、排序字段等。
- **排序** 对于返回列表数据的排序排序规则和SQL的排序类似(通过指定字段以及升序|降序字段可以组合ReadPlan支持有**两种**方式:
- **默认排序**:指定默认排序字段(不需要入参指定排序字段)
- **自定义排序**:指定排序字段(需要入参指定排序字段),这种方式通常能很好的满足列表头动态指定排序的需求
- **排序字段的来源**并不是查询字段中的所有字段都能作为排序字段因为返回的根DTO|VO的id去重列表所以排序字段只能来源自根WO不包括列表WO属性: 比如MeetingWO包含了List<MeetingAgendaWO> agendaList属性 那么agendaList不能作为排序字段以及根WO扩展出来的非列表属性的WO的字段比如MeetingWO包含了MeetingRoomWO,那么MeetingRoomWO的属性也能作为排序字段**注意**为了唯一确定是给按照那个字段进行排序排序字段是一个从根节点到当前属性的路径例如ADto包含了BDto bDtoBDto具备属性name如果指定对BDto的name字段排序则字段路径为bDto.name
- 你需要提取需求中的查询部分信息,以输入的查询对象作为查询上下文件,构建一个查询语句
- 如果有列表属性过滤需求(如果没有filter对每个列表属性都是放回全部数据例如MeetingDto的MeetingAgendaDtoList属性如果不设置MeetingAgendaDtoList的filter则返回该会议的全部议程信息):你需要提取需求中的过滤部分信息,以输入的查询对象作为查询上下文件,针对可过滤字段(列表属性)分别创建过滤条件, **注意** 过滤条件不能使用列表属性作为查询条件属性即不能使用contains语法为了唯一确定是给那个列表属性的指定过滤条件属性字段是一个从根节点属性到当前属性的路径例如ADto包含了List<BDto> bDtoList;BDto包含了List<CDto> cDtoList如果对cDtoList使用Filter则字段路径为bDtoList.cDtoList
- 查询语法和过滤语法
- 基本语法格式是 属性名 操作符 变量或常量,变量表示了这个数据是外部传入的,用 #变量名为格式;如果是枚举类型常量,需要使用'符号包起来,例如: 男性枚举类型 'MALE'
- 对于数值、时间类型属性有:!=, ==, >, <,<=,>=, in,notIn, isNullOrNot 这些操作符
- 对于文本类型属性有like, isNotNull, isNull,!=, ==, in,notIn, isNullOrNot 这些操作符
- isNullOrNot操作符是一个三元操作符用于根据入参判断过滤某个值是否为null。举例user对象有gender字段如果定义了语句 gender isNullOrNot #genderIsNullOrNot, 如果入参 genderIsNullOrNot为true等价于条件 where gender is null 如果入参#genderIsNullOrNot为false等价于 where gender is not null
- 对于对象属性或者列表对象属性可以用isNull, isNotNull, isNullOrNot 这些操作符。注意本对象不能直接用这个操作符。
- 对于对象属性还可以对其子属性进行上述查询
- 查询变量不能作为条件属性
- 查询条件之间可以使用AND, OR 进行连接
- 可以插入括号()对条件进行分组
- 对于列表对象属性只能使用contains、isNull、isNotNull操作符如果是wo列表类型可以使用contains(子查询, 子查询的条件属性只能作用在对应的列表类型对象的属性上),表示了该列表属性中需要包含至少一个满足子查询条件的对象; 其他列表类型只能使用isNull或者isNotNull
- 把上述子查询通过and, or, not这三个连接符拼装在一起就可以完成一个查询。请不要使用没提示过的操作符号连接符。
- 查询条件中的入参可以在运行时传入或者不传入值如果不传入值表示该参数相关的条件不起作用基于这种查询语句实际运行时的动态效果多个条件联合查询的时候可以优先使用AND
- 使用点号(.)可以访问当前对象的单值对象类型的子属性, 可以多个点号的组合访问嵌套单值对象的属性
- 查询条件中的属性必须是当前查询对象的属性或单值对象属性或者单值对象的子属性
- 禁止使用filter语法
- 禁止使用has语法
- 语法定义:使用 lezer 定义了如下语法
```
@top Program { expression? }
@skip { spaces | newline | LineComment }
@precedence {
member,
and @left,
or @left
}
kw<term> { @specialize[@name={term}]<identifier, term> }
boolean { @specialize[@name=Boolean]<identifier, "true" | "false"> }
@skip {} {
String[isolate] {
'"' (stringContentDouble | Escape)* ('"' | "\n") |
"'" (stringContentSingle | Escape)* ("'" | "\n")
}
}
commaSep<content> {
content ("," content)*
}
List {
"[" commaSep<Value> ~destructure "]"
}
Value { String | Number | List | boolean }
Field { identifier ~arrow }
Member { Field !member "." (Member | Field) }
Input { "#" identifier ~arrow }
expression {
TupleExpression | BinaryExpression | NotExpression | ParenthesizedExpression | ListExpression
}
TupleExpression { TwoTupleExpression | ThreeTupleExpression }
TwoTupleExpression { (Field | Member) TwoOperator }
ThreeTupleExpression { (Field | Member) ThreeOperator (Input | Value) }
ListExpression { (Field | Member) ListOperator ParenthesizedExpression }
BinaryExpression {
expression !and (kw<'AND'> | kw<'and'>) expression |
expression !or (kw<'OR'> | kw<'or'>) expression
}
NotExpression { (kw<'NOT'> | kw<'not'>) ParenthesizedExpression }
ParenthesizedExpression { "(" expression ")" }
@tokens {
spaces[@export] { $[\u0009 \u000b\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+ }
newline[@export] { $[\r\n\u2028\u2029] }
identifierChar { @asciiLetter | $[_$\u{a1}-\u{10ffff}] }
word { identifierChar (identifierChar | @digit)* }
identifier { word }
hex { @digit | $[a-fA-F] }
stringContentSingle { ![\\\n']+ }
stringContentDouble { ![\\\n"]+ }
@precedence { spaces, newline, identifier }
Escape {
"\\" ("x" hex hex | "u" ("{" hex+ "}" | hex hex hex hex) | ![xu])
}
Number {
(@digit ("_" | @digit)* ("." ("_" | @digit)*)? | "." @digit ("_" | @digit)*)
(("e" | "E") ("+" | "-")? ("_" | @digit)+)? |
@digit ("_" | @digit)* "n" |
"0x" (hex | "_")+ "n"? |
"0b" $[01_]+ "n"? |
"0o" $[0-7_]+ "n"?
}
@precedence { Number "." }
ThreeOperator { "in" | "notIn" | "!=" | "==" | ">" | ">=" | "<" | "<=" | "isNullOrNot" | "like" | "has" }
TwoOperator { "isNull" | "isNotNull" }
ListOperator { "contains" | "filter" }
LineComment[isolate] { "//" ![\n]* }
"(" ")"
"[" "]"
"."
}
@detectDelim
```
- **读方案设计元素的表达**
- 以json格式表达json schema 定义如下
```json
{
"type":"object","description":"读方案定义",
"properties":{
"qto": {
"type":"object",
"properties":{
"name": {"type":"string","description":"读方案名称表达了查询的意图英语描述单词之间使用下划线分割长度限制在32个字符内"},
"uuid": {"type":"string","description":"读服务设计元素的uuid创建时不传入在更新的时候必须传入"},
"woId": {"type":"string","description":"查询对象的uuid作为查询服务的上下文创建的时候必须指定更新的时候不传递"},
"description": {"type":"string","description":"描述读方案的功能长度限制在256个字符内"},
"generateCountApi": {"type":"boolean", "description":"是否需要生成计数接口"},
"supportPaginate": {"type":"boolean", "description":"是否需要分页"},
"supportUnPage":{"type":"boolean", "description":"如果不需要分页一次性返回部数据则返回true"},
"supportWaterfall":{"type":"boolean","description":"是否需要瀑布流"},
"query":{"type":"string","description":"查询语句,符合前述语法定义"},
"voOrDtoId":{"type":"string","description":"返回数据对象(VO或DTO)的uuid,创建的时候必须指定,更新的时候不传递"},
"outOrder":{"type":"array","description":"入参排序方式定义",
"items":{"type":"object","description":"定义字段排序方式","required":["fieldPath", "direction"],
"properties":{"fieldPath": {"type":"string","description":"字段名路径"},"direction": {"type":"string","description":"排序方向可以是ASC(升序)或者DESC(降序)"}}}
},
"defaultOrder": {"type":"array","description":"默认排序方式定义",
"items": {"type":"object","description":"定义字段排序方式","required":["fieldPath", "direction"],
"properties":{"fieldPath": {"type":"string","description":"字段名"},"direction": {"type":"string","description":"排序方向可以是ASC(升序)或者DESC(降序)"}}}
},
"filters": {"type": "array", "description":"过滤条件定义",
"items": {"type":"object","description":"定义过滤条件","required":["fieldPath", "filter"],
"properties":{
"fieldPath": {"type":"string","description":"需要过滤的列表字段路径"},
"filter": {"type":"object","description":"过滤语法,符合前述的语法定义,不能使用contains语法"}
}
}
},
"required":["name","description","query"]
}
},
"required":["qto"]
}
}
```
- **举例**
- 上下文
```java
public class meeting_room {
storey storey; // 楼层id
String name; // 名称
room_type_enum room_type; // 会议室类型
List<equipment_enum> equipment; // 设备
Long id; // 主键
Long seat_number; // 座位数
String description; // 说明
Boolean enable_indicator; // 是否启用
String input_code; // 输入码
Long storey_id; // 楼层id
Long contact_staff_id; // 联系员工ID
List<meeting> meeting; //注意:该字段可添加过滤条件
class storey { // 建筑的楼层
location location; // 位置id
String name; // 楼层
Long id; // 主键
Long sort_number; // 排序号
Long location_id; // 位置id
}
class meeting {
Long id; // 主键
String title; // 标题
Date start_time; // 开始时间
Date end_time; // 结束时间
Long room_id; // 会议室id
String description; // 描述
Long create_user_id; // 创建人id
}
class location { // 位置信息,如楼栋
String name; // 名称
Long id; // 主键
String description; // 描述
Long sort_number; // 排序号
}
enum room_type_enum { //
SMALL, //小会议室
MEDIUM, //中会议室
LARGE
}
enum equipment_enum { //
TV, //电视
WHITEBOARD, //白板
PROJECTOR, //投影仪
VIDEO
}
}
```
- 用户需求
获取一段时间未被使用的会议室(包含当前会议已选择的会议室),并且根据会议名称会议信息,最后分页返回。
- 对应的读方案定义
```json
{
"name": "get_unused_meeting_room_list",
"description": "获取未使用的会议室(包含当前会议已选择的会议室)",
"woId":"759bedd4-4540-4de6-b65d-d44912fb0991",
"dtoOrVoId":"e50c02c3-f336-4dc6-88e1-24ffe3dea1e2",
"generateCountApi": true,
"supportPaginate": true,
"supportUnPage": false,
"supportWaterfall": false,
"query": "enable_indicator == true AND ( id == #idIs OR meeting isNull OR NOT ( meeting contains ( start_time <= #meetingEndTime AND end_time >= #meetingStartTime ) ) )",
"filters": [
{
"fieldPath": "meeting",
"filter": "title like #meetingTitleLike"
}
]
}
```
- **代码产物和修改建议**
- **查询传输对象(QTO)**
* **生成产物:** 在Service层生成一个Java类
* **命名规则:** 类名以Qto结尾(${ReadPlanName}Qto)
* **类路径:** 位于 ```**.persist.qto```包路径下
* **职责:** 读方案的查询参数结构,通常可作为API的参数API接收到参数后可直接透传给内部的RPC进行调用
* **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${ReadPlan在TOCO中的uuid}|QTO|DEFINITION
* **内部结构:** QTO内部包含了如下分页相关的字段:
```
private String scrollId; //瀑布流分页时用作游标
private Integer size; //页码分页时用作每页记录的数量
private Integer from; //页码分页时用作开始的记录位置
```
- **Dao**
- **生成产物:** 在Dao层生成一个Java类
- **命名规则:** 类名以Dao结尾(${ReadPlanName}Dao)
- **类路径:** 位于 ```**.persist.mapper```包路径下
- **职责:** 读方案对应的数据库查询方法
- **唯一标识符位置:** 其对应的唯一标志在类注解@AutoGenerated中指定,uuid规则: ${ReadPlan在TOCO中的uuid}|QTO|DAO
- **QueryExecutor**
- **生成产物** 对于返回**VO**的查询方案在controller层生成一个Java类,对于分页查询、不分页返回全量、瀑布流查询、计数都有后独立的函数实现
- 分页查询, 会生成一个 **Paged函数, 返回VSQueryResult<XxxVo>
- 不分页返回全量, 会生成一个query**函数返回List<XxxVo>
- 瀑布流,会生成一个,**Waterfall函数返回VSQueryResult<XxxVo>
- 查询数量, 会生成一个 **Count函数返回Integer
- **命名规则:** 类名以QueryExecutor结尾(${VoName}QueryExecutor)
- **类路径:** 位于 ```**.entrance.web.query.executor```包路径下
- **职责:** 提供针对VO的查询入口把QtoService返回的id数据转化成目标**VO**
- **QueryService**
- **生成产物** 对于返回**DTO**的查询方案在service层生成一个Java类,对于分页查询、不分页返回全量、瀑布流查询、计数都有后独立的函数实现
- 分页查询, 会生成一个 **Paged函数, 返回VSQueryResult<XxxDto>
- 不分页返回全量, 会生成一个query**函数, 返回List<XxxDto>
- 瀑布流,会生成一个,**Waterfall函数, 返回VSQueryResult<XxxDto>
- 查询数量, 会生成一个 **Count函数,返回Integer
- **命名规则:** 类名以QueryService结尾(${DtoName}QueryService)
- **类路径:** 位于 ```**.service.index.entity```包路径下
- **职责:** 提供对DTO查询入口把QtoService返回的id数据转化成目标**DTO**,或返回符合条件的记录数量
- **VSQueryResult说明**
- VSQueryResult为一个写死的类全路径为com.vs.es.query.VSQueryResult存在于jar中可以直接使用禁止再创建新的VSQueryResult类!
- VSQueryResult代码结构如下
```
package com.vs.es.query;
import lombok.Data;
import java.util.List;
@Data
public class VSQueryResult<T> { //T为DTO或VO类型
private int count;
private List<T> result;
private int from; //页码分页的起始条数
private int size; //每页的记录数量
private String scrollId; //瀑布流查询游标
private boolean hasMore; //瀑布流查询时表示是否有下一页
}
```
- **例子:**
* 根据用户名称查询用户列表返回UserDTO则生成UserNameQto、UserNameQtoService、UserNameQtoDao、UserNameQueryService; UserNameQueryService调用UserNameQtoServiceUserNameQtoService调用UserNameQtoDao
- **修改建议:**
- 如果有对结果的数据二次处理建议在QueryService和QueryExecutor中进行代码扩展不建议修改QTO文件
#### **2.10 查询传输对象(QTO)**
- **定义与用途:** 在TOCO中QTO为读方案的查询参数结构每个读方案会对应一个QTO读方案调用方按照QTO的结构向读方案生成的RPC方法传入需要查询的实体字段值完成对数据库的查询
- **如何创建/生成:** 在创建读方案后TOCO会自动生成QTO作为该读方案传入的查询参数结构无需单独创建
- **关键配置:** 名称(${ReadPlanNameQto},驼峰展示),查询字段列表(如idIsnameLike, schoolNameLike等)
- **与API的关系:** QTO通常可作为API的参数API接收到参数后可直接透传给内部的RPC进行调用注意QTO只用作读操作的参数**禁止用作写参数结构**
#### **2.11 写方案 (WritePlan单聚合操作)**
- **定义与用途:** TOCO针对写场景定义了一种写方案所有对数据库的写操作都只能通过写方案实现一个写方案只可以变更一个聚合的数据无法同时操作多个聚合!。写方案包含了对数据库表的写操作。每个写方案只能操作一个聚合内部的表同时对一个聚合内表的操作尽量合并至一个写方案中根据复用性、内聚性等方面具体情况具体分析。注意写方案可以一次操作聚合内的多张表如location和storey是同一个聚合并且location和storey是**1:N**关系location是父对象聚合根storey是子对象那么我们可以创建一个写方案同时更新单个聚合根location中的信息以及操作其下的子表storey**列表**信息如新增、删除、修改storey等也可以创建一个写方案单独更新单个子表storey对象信息。
- **关键配置:** 名称(小写字母+下划线不要以write_plan结尾,全局唯一)操作的聚合聚合内的实体和字段对每个实体的操作类型CREATE创建**单个**实体UPDATE(更新**单个**实体), DELETE(删除**单个**实体), CREATE_ON_DUPLICATE_UPDATE(创建或者更新**单个**实体),FULL_MERGE( 批量更新**列表**数据根据传入的列表数据一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作并且删除掉老的列表中不在传入列表数据中的部分), PARTIAL_MERGE(批量更新**列表**数据,根据传入的列表数据,一条一条执行)
- **与RPC的关系:** 对于每一个写方案TOCO会自动生成一个RPC方法其参数为写方案对应的BTO返回值为本次操作的聚合根实体的主键值内部只实现了对当前聚合的数据库操作
- **生成的写方案RPC** 的使用: 写方案生成的RPC属于该写方案所属的模块其他模块如果需要调用该写方案需要先订阅该写方案RPC在生成代码后使用adapter调用该读服务当前模块则可以直接调用该写方案对应的Service
- **与聚合的关系:** 每个聚合下可以定义多个写方案,但每个写方案只能操作一个聚合内的表,无法同时操作多个聚合内的表
- **如何创建/生成:** 创建写方案时需要先选定对应的聚合,以及要操作的聚合内部的实体,然后确定对每个实体的具体操作类型
- 提取需求中的更新部分需求,根据上下文信息构建出一个写更新方案
- 上下文中输入了备选聚合对象的的定义,每个聚合对象定义了多个实体的嵌套关系
- 先明确聚合内实体的父子关系层级
- 根据需求提取出变更相关的名称
- 根需求提取出变更相关的描述
- 根据需求选择确定变更范围,选出一个最合适的聚合对象;注意: 最多只能选择一个聚合对象
- 根据需求在选定的聚合对象按照对象的粒度,选出所需变更对象
- 根据需求选定变更对象的字段;每个选定的对象至少需要选定一个字段,如果无法确定任何字段,默认选择全部字段, 字段必须明确指定名称,不能类似“所有字段”的模糊表达
- 每个对象上可以设置对应的一个操作操作包括CREATE创建UPDATE(更新), DELETE(删除), CREATE_ON_DUPLICATE_UPDATE(创建或者更新),FULL_MERGE( 批量更新列表数据根据传入的列表数据一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作并且删除掉老的列表中不在传入列表数据中的部分), PARTIAL_MERGE(批量更新列表数据根据传入的列表数据一条一条执行CREATE_ON_DUPLICATE_UPDATE的操作)
- 列表属性可以批量操作
- 批量更新列表数据(FULL_MERGE或者PARTIAL_MERGE),则操作方案需要包含它的父对象
- 批量删除列表数据则操作方案需要包含父对象父对象的操作设置为UPDATE
- 批量创建列表数据则操作方案需要包含父对象父对象的操作设置为UPDATE、或者是CREATE当父对象也需要一起创建的时候
- 举例meeting对象有List<meeting_agenda> agdendaList属性如果需要批量创建agenda则需要同时设置metting对象和meeting_agenda对象并且对meeting_agenda设置create操作这是会生成一个Bto参数其中包含了 List<meeting_ageda> agendaList的属性用于批量传递参数反正如果只设置了meeting_agenda的create操作生成的Bto参数只包含了meeting_agenda实体的属性表示一次只能创建一条数据 同理对于UPDATE、DELETE也是类似的规则
- FULL_MERGE和PARTIAL_MERGE只能用在列表属性上, 单值属性请使用CREATE_ON_DUPLICATE_UPDATE
- 操作是 UPDATE、DELETE、CREATE_ON_DUPLICATE_UPDATE、FULL_MERGE、PARTIAL_MERGE的时候必须且只能指定一个唯一键(unique_key)(包括主键),用于确定对应的数据记录主键对应的字段也要包含在fields字段中;
- 唯一键(unique_key)的具体解释:在实体中,存在普通索引和唯一索引,这里的唯一键值的是唯一索引,由一个或多个字段组成
- 选定的对象层级不能跳跃;如果发生不连续,去掉不连续的叶子对象
- 注意父对象和下一层子对象的操作类型有如下限制,必须严格检查以下规则
- 如果父对象的操作是DELETE则下一层子对象不能选择也不能能设定任何操作
- 如果父对象是的操作是CREATE则下一层子对象只能选择CREATE
- 如果父对象是CREATE_ON_DUPLICATE_UPDATE子对象只能是CREATE或者CREATE_ON_DUPLICATE_UPDATE或者FULL_MERGE或者PARTIAL_MERGE
- 如果父对象是FULL_MERGE子对象只能是FULL_MERGE或者PARTIAL_MERGE或者是CREATE
- 对于选定的字段列表中的字段如果字段的类型是Long、BigDecimal、Float 则可以设置为增量更新字段, 把值设置在incrFields属性中和操作"UPDATE"、"CREATE_ON_DUPLICATE_UPDATE"、"PARTIAL_MERGE"或者"FULL_MERGE"搭配表示对该字段增量更新: 例如A实体的字段count类型是Long, 如果该字段被设置为增量跟新字段最终的效果是A.count = A.count + ? ; 对于减少字段的值也用该方法表示,可以通过传入负值的入参达到减值的效果
- **写方案设计元素的表达**:
以json格式表达json schema 定义如下:
```json
{
"type":"object",
"description":"写方案定义",
"properties":{
"uuid":{"type":"string","description":"写方案设计元素的唯一标识,在创建的时候不传;更新的时候必须传递"},
"name":{"type":"string","description":"写方案名称英语表示单词之间使用下划线连接长度限制在32个字符之内"},
"description":{"type":"string","description":"写方案描述长度限制在256个字符内"},
"bo": {"type":"string","description":"聚合根的名称,确定了唯一聚合"},
"operations": {
"type":"array","description":"定义了对写方案涉及到的相关实体的操作,包括操作类型和相关字段",
"items":{
"type": "object", "description":"定义了对具体一个实体的操作,包括操作类型和相关字段",
"properties":{
"bo":{"type":"string","description":"具体操作的实体"},
"action":{"type":"string","description":"对具体实体的操作"},
"uniqueKey": {
"type":"array",
"description":"唯一键包含的字段列表,该唯一键用于确定数据记录",
"items":{"type":"string","description":"字段名称,组成唯一键的字段名称"}
},
"fields":{
"type":"array",
"items":{"type":"string","description":"字段名称必须来自bo对象"},
"description":"对选定的Bo的操作的字段列表"
},
"incrFields":{
"type":"array",
"items":{ "type":"string", "description":"增量字段名称"},
"description":"选定的字段中进行增量操作的字段列表"
},
"required":["bo","action","fields"]
}
}
}
},
"required":["name","description","operations","bo"]
}
```
- **例子**
- **上下文:**
```java
public class meeting {//会议聚合
//唯一键:(id)、(meeting_name)
Long id; //主键id
String meeting_name; //会议名称
Date start_time; //开始时间
Date end_time;//结束时间
List<meeting_agenda> meetingAgendaList;
class meeting_agenda {
Long id;//主键
Date start_time;//开始时间
Date end_time;//结束时间
String title;//议程名称
List<agenda_vs_user> agendaVsUserList;
}
class agenda_vs_user {
Long id; //主键
Long user_id; //用户id
Long agenda_id; //议程id
Date enroll_time; //注册时间
Long meeting_id;//会议id
}
}
public class user {//用户聚合
//唯一键:(id)、(name,gender)
Long id;//主键
String name;//名字
GenderEnum gender;//性别
String nickname;//昵称
Long friend_count;//好友数量
}
```
- **需求:** 创建会议
- 对应的写方案定义:
```json
{
"name": "create_meeting",
"description": "创建会议",
"bo": "meeting",
"operations": [
{
"bo": "meeting",
"action": "CREATE",
"fields": [
"meeting_name",
"start_time",
"end_time"
]
}
]
}
```
- **代码产物和修改建议**
- **BOService:**
* **生成产物:** 在对应的聚合服务BOService里生成一个函数
* **函数命名规则:** 和写方案同名
* **职责:** 按需对入参进行转换调用BaseBOService里的函数完成对聚合对象的操作, 实现对数据库的写操作;
* **返回值:** 操作的聚合根实体记录的主键字段的值
* **BOService的类路径:** ```**.service```
* **唯一标识符位置:** 其对应的标识符在函数的注解@AutoGenerated中指定, uuid规则: ${WritePlan在TOCO中的uuid}
- **BaseBOService**
- **生成产物:** 在对应的聚合的BaseBOService里生成一系列函数根据入参完成对聚合对象的变更
- **职责** 增对每个实体的生成增删改的函数,并且根据参数的结构以及聚合的结构,构建嵌套的调用逻辑,完成对一个聚合对象的变更,记录并且返回对应的变更情况
- **类路径:** ```**.service.base```
- **禁止** 修改该类
- **BTO (业务变更传输对象)**
* **生成产物:** 一个Java类以内部类的方式表示层级结构, **注意** BTO只能由写方案生成不能凭空创建
* **命名规则:** 一个Bto结尾(${WritePlanName}Bto驼峰展示)
* **禁止** 修改该类
* **类路径:** 位于```**.service.bto```包路径下
* **职责:** 在**TOCO**中BTO为写方案的参数结构每个写方案会对应一个BTO写方案调用方按照BTO的结构向写方案生成的RPC方法传入需要查询的实体字段值完成对数据库的变更
* **唯一标识符位置:** 其对应的唯一标识符在类注解@AutoGenerated中指定, uuid规则: ${WritePlan在TOCO中的uuid}|BTO|DEFINITION
- **例子:**
- 创建用户以及用户设置的写方案create_user_and_setting生成CreateUserAndSettingBto, 在用户的聚合(UserBO)对应的UserBOService中生成函数createUserAndSetting该函数调用BaseUserBOService中生成的createUserAndSetting, 其中在BaseUserBOService中还生成了createUser和createSetting的函数, 一起完成了用户的创建和设置创建的逻辑。
- **修改建议:**
- 不能修改BaseBOService中的函数不建议修改BTO文件。建议在BOService中进行手动代码扩展处理可能被复用的修改前后的逻辑如修改数据库的前后值对比、或常被复用的校验逻辑业务不变性校验逻辑除外、需要经常在一个事务内执行的其他写操作等。
- 父类函数会返回一个BoResult类此类记录了各个Bto和Bo实例的对应关系以及各个Bto实例的操作结果, 可以通过以下接口获取。 例如在创建用户的写方案中BoService里的入参为CreateUserBto 例如在创建用户的写方案中BoService里的入参为CreateUserBto,用户id由数据库生成如果需要返回创建用户id那么通过boResult.getAddedResult(createUserBto).getBo().getId()可以返回新建的用户id
```
/**
* 获取更新成功的bto结果
*
* @return
*/
public UpdatedBto getUpdatedResult(final Object bto)
/**
* 获取成功插入的Bto的结果
* @param btoObj
* @return
*/
public AddedBto getAddedResult(final Object btoObj)
/**
* 获取Bto对应的删除结果
*
* @param btoObj
* @return
*/
public DeletedBto getDeletedResult(final Object btoObj)
```
```
public class UpdatedBto<Bto,Entity,BO> {
//Bto 入参
private Bto bto;
//Bto 对应的Entity前项
private Entity entity;
//bo后项
private BO bo;
}
```
```
//记录删除记录的情况
@Setter
@Getter
public class DeletedBto<Bto,Entity> {
//Bto 入参
private Bto bto;
//Bto 对应的Entity前项
private Entity entity;
}
```
```
//记录Bto创建的前后值
@Setter
@Getter
public class AddedBto<Bto,BO> {
//Bto 入参
private Bto bto;
//bo 后项
private BO bo;
}
```
#### **2.12 业务变更传输对象(BTO)**
- **定义与用途:** 在TOCO中BTO为写方案自动生成的参数结构每个写方案会生成一个BTO。BTO为写方案选定的操作实体根据关系形成的树形集合最外层为聚合根。写方案调用方按照BTO的结构向写方案生成的RPC方法传入需要操作的实体字段值完成对数据库的写操作
- **如何创建/生成:** 在创建写方案后TOCO会自动生成一个BTO作为该写方案传入的参数结构无需通过TOCO创建。
- **关键配置:** 名称(${WritePlanName}Bto驼峰展示嵌套的树形实体和字段列表BTO内部的字段全部都来自Entity。以下为一个示例
```
class CreateUserBto { //对应实体user
Long id; //来自于user.id
String name; //来自于user.name
List<PictureBto> pictureList;
class PictureBto { //对应实体picture
String url; //来自于picture.url
}
}
```
- **与API的关系:** BTO通常可作为API的参数API接收到参数后可直接透传给内部的RPC进行调用注意BTO只用作写操作的参数**禁止用作查询参数结构**
- **复杂场景处理:** 对于涉及多个写方案的复杂API可以
a. 创建一个主要的写方案让其BTO作为API入参
b. 使用基本类型参数在流程中转换为各个写方案的BTO
c. 如果流程过于复杂,优先考虑使用基本类型参数
#### **2.13 服务层方法 (RPC)**
- **定义与用途:** 在TOCO中RPC为服务层的方法。RPC按照可见性可以分为两种一种是公开RPC可以被其他模块订阅订阅后可以通过RPC适配器进行调用另一种是非公开RPC只能被当前模块调用。非公开RPC可以被公开从而被其他模块订阅并调用
- **如何创建/生成:** RPC有4种创建方式a.DTO创建后会自动创建RPCRPC的公开性与DTO的公开性保持一致b.返回DTO的读方案会根据分页情况、以及是否生成计数函数的配置自动生成非公开的RPC c.写方案创建后会自动生成非公开的RPC返回值为操作的聚合根实体记录的主键字段的值 d.如果上述三种RPC无法满足需求则可以通过TOCO创建自定义RPC完整指定功能需指定具体的参数和返回值以及公开性等。
- **优先复用:** 当用户需要创建一个RPC时如果用户有明确要求创建的方式则按照用户的要求来创建。如果没有明确要求则通常先判断是否可以通过创建读方案、写方案、DTO来使TOCO自动创建出对应的RPC最后再考虑通过TOCO创建自定义RPC
- **自定义RPC和代码中手写方法的关系** 二者都可以通过手动的方式实现一个服务层的方法应用场景的区别在于如果一个方法需要被其他模块订阅则通常使用自定义RPC如果一个方法只是某个API私有调用不需要给外部模块开放则可以使用代码手写方法
- **关键配置:** 类名(驼峰首字母大写以Service结尾)、是否公开、方法名(驼峰,首字母小写)、请求参数、返回值。注意如果一个RPC是分页查询且参数为Qto类型则Qto中已经包含了分页所需的from、size、scrollId等属性无需额外为RPC增加类似参数。
- **TOCO中RPC的存储:** 注意RPC在TOCO中只存储了方法签名不存储内部的执行流程逻辑如果需要了解其内部的实现逻辑则需要通过阅读RPC对应的代码。
- **参数类型:** RPC的参数**只能**为QTO、BTO、Enum、基本类型可为单值或列表。注意如果是对象类型则优先使用QTO、BTO作为参数**禁止使用**VO和自定义结构如Object
- **返回值类型:** RPC的返回值**只能**为DTO、Enum、基本类型,可为单值或列表,**禁止使用**VO、QTO、BTO、自定义结构如Object作为返回值。注意如果是对象类型则优先使用DTO作为返回值
- **TOCO中json结构描述:** 在TOCO中DTO使用一个json结构表示示例如下
```json
{
"methodName": "saveUsers",
"className": "UserSaveService",
"requestParams":[
{
"name": "saveUserBtoList",
"description": "批量保存用户参数",
"type": "List",
"innerType": "Bto",
"innerUuid": "dbvvc4d4-0063-442f-abd7-vrfded656988"
}
],
"response": {
"type": "Boolean"
}
}
```
结构中一些关键字段描述如下:
requestParams为请求参数列表response为返回结构requestParams中每个参数和response的结构相同其中name为参数名;type为参数类型参数类型取值范围为Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,Dto,Qto,Bto,List,PageResult,Void其中参数不能为Void和PageResult如果不需要返回值则type设置为Void如果返回值为分页查询的结果则type设置为PageResult且innerType必为Dto对应代码中的VSQueryResult<XxxDto>description为描述typeUuid为参数对应类结构的UUID当type为Enum、Eo、Dto时传入该对象的uuid当type为Qto时传入对应读方案的uuid、当type为Bto时传入对应写方案的uuidinnerType为List内部类型当type为List或PageResult时包含该字段innerUuid为List内部类结构的UUID当type为List或PageResult且innerType为Enum、Eo、Dto时传入该对象的uuid当innerType为Qto时传入对应读方案的uuid、当innerType为Bto时传入对应写方案的uuid。
- **生成代码:** RPC会在service层中生成类文件及实现函数包含DTO自动生成的RPC如UserDtoService.getById(主键为id)或StaffBaseDtoService.getByStaffId(主键为staff_id)、读写方案自动生成的RPC如UserDtoQueryService.queryByListQto、UserBOService.createUser、自定义RPC如UserCustomService.customMethod。特别注意公开的RPC才可被其他模块使用RPC被订阅后会生成RpcAdapter适配器其他模块通过RpcAdapter才可调用该方法。如Order模块订阅了User模块的UserDtoService.getById则会在Order模块中生成UserDtoServiceInOrderRpcAdapter.getById方法Order模块中的代码必须通过@Resource private UserDtoServiceInOrderRpcAdapter userDtoServiceInOrderRpcAdapter;注入适配器后才可进行方法调用。这里**必须要注意**:变量的命名必须是类名的首字母小写,禁止使用其他变量名。
- **修改建议:** 建议修改RPC方法不建议修改RPC方法签名、适配器中的内容
#### **2.14 应用程序接口 (API)**
- **定义与用途:** 在TOCO中API用于定义对外暴露的HTTP接口
- **如何创建/生成:** API一般为通过TOCO创建需指定具体的参数和返回值等。
- **TOCO中API的存储:** 注意API在TOCO中只存储了其URI和方法签名不存储内部的执行流程逻辑如果需要了解其内部的实现逻辑则需要通过阅读API对应的代码。
- **关键配置:** uri(加粗展示,一般为/api/${moduleName}/xxx如/api/user/create,全局唯一。如果用户有特殊命名规则的话以用户要求为准)、类名(以Controller结尾)、方法名(驼峰,首字母小写)、请求参数、返回值。注意如果一个API是分页查询且参数为Qto类型则Qto中已经包含了分页所需的from、size、scrollId等属性无需额外为API增加类似参数。
- **参数类型:** API的参数**只能**为QTO、BTO、Enum、基本类型可为单值或列表。注意如果是对象类型则优先使用QTO、BTO作为参数。**禁止使用**DTO和自定义结构如Object
- **返回值类型:** TOCO的API运行在自己的Java脚手架中脚手架会自动对API的返回值做一层对象包装code、message、data。所以在TOCO中API的返回值无需考虑返回码和错误信息只需考虑返回的数据本身。TOCO中API的返回值**只能**为VO、Enum、基本类型,可为单值或列表,**禁止使用**DTO、QTO、BTO、自定义结构如Object作为返回值。注意如果是对象类型则优先使用VO作为返回值。
- **TOCO中json结构描述:** 在TOCO中API使用一个json结构表示示例如下
```json
{
"methodName": "getUserMeetingList",
"className": "MeetingQueryController",
"usageScenario": "xxx",
"requestParams":[
{
"name": "getMeetingListByUserId74Qto",
"description": "查询参数",
"type": "Qto",
"uuid": "6351a4d4-0063-442f-abd7-a2df6d656988"
},
{
"name": "test",
"description": "是否为测试请求",
"type": "Boolean"
}
],
"response": {
"type": "List",
"innerType": "Vo",
"innerUuid": "1d58352b-5333-4509-aec2-1abc3fac9122"
}
}
```
结构中一些关键字段描述如下:
requestParams为请求参数列表response为返回结构requestParams中每个参数和response的结构相同其中name为参数名;type为参数类型参数类型取值范围为Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,List,PageResult,Vo,Qto,Bto,Void其中参数不能为Void和PageResult如果不需要返回值则type设置为Void如果返回值为分页查询的结果则type设置为PageResult且innerType必为Vo对应代码中的VSQueryResult<XxxVo>description为描述typeUuid为参数对应类结构的UUID当type为Enum、Eo、Vo时传入该对象的uuid当type为Qto时传入对应读方案的uuid、当type为Bto时传入对应写方案的uuidinnerType为List内部类型当type为List时包含该字段innerUuid为List或PageResult内部类结构的UUID当type为List或PageResult且innerType为Enum、Eo、Vo时传入该对象的uuid当innerType为Qto时传入对应读方案的uuid、当innerType为Bto时传入对应写方案的uuid。
- **代码产物和修改建议**
- **生成代码:** API会在entrance层生成Controller以及对应的API方法
- **修改建议:** 建议修改API方法的实现内容禁止直接修改API方法签名、URI**注意**如需修改API定义名称、出入参数、URL需要通过修改API设计元素实现
#### **2.15 流程服务FunctionFlow)**
- **定义与用途:** TOCO针对复杂业务拆解定义了流程服务把一个复杂的业务过程根据业务逻辑的内聚性合并逻辑功能把流程分解成流程节点最终构造出一个类似工作流的逻辑流程最终实现复杂业务流程分解提升代码的可维护性。TOCO内嵌了流程引擎在Function_Flow生成代码后可以在流程引擎中执行
- **何时使用:**
- 如果一个API/RPC中涉及的写服务超过3个则**必须**使用流程服务
- 当用户要求使用流程服务
- **节点的封装:**
- 因为TOCO是一个面向数据处理的系统所以数据内聚为首要考虑因素一个节点通常围绕一个核心写服务包括取数的读服务为写服务的入参进行数据处理和转换以及该写服务完成后的一些附属功能, 除了条件节点,入参校验,最终数据返回节点,每个节点至少包括一个写服务。
- **关键配置:** 名称(小写字母+下划线),拆解复杂业务逻辑,如果业务流程比较简单,则不需要使用流程服务
- **如何创建/生成:**
- 流程不是逻辑的伪代码,不需要表达全部逻辑细节,相关有内聚性,相似性的功能逻辑需要被封装到一个流程节点内;例如用户注册:创建用户需要,需要创建账号信息,需要创建用户信息,并且发送一个通知消息,这几个功能都属于创建用户相关信息,而且没有逻辑分叉,因此可以内聚在一个逻辑节点内。
- 流程节点之间的参数和返回值的传递通过一个统一的上下文Context传递在写代码的时候按需修改Context文件添加所需的字段
- 定义了JSON结构用于表达逻辑流。流程节点分为 “顺序节点”、“条件节点”,“选择节点”,"开始节点“ ,节点之间通过有向边连接,表示逻辑的执行方向;从开始节点开始,可达各个节点。
- 节点的边的定义如下顺序节点可以有多条入边可以有多条出边一个出边表示下一个执行的节点多条出边表示几个分支并发执行条件节点可由多条入边可以有2条出边表示条件为True和False两种逻辑分支选择节点可以有多条入边可以有多条出边每条出边表示一种选择路径开始节点只能有一条出边不能有入边;
- ”条件节点“只封装条件判断逻辑返回TRUE或者FALSE”循环节点“只封装条件判断逻辑返回TRUE或者FALSE”选择节点“只封装分支选择逻辑返回下游分支节点的名称
- 终止流程:直接抛出异常
- 一般校验逻辑放在一个“顺序节点"中,如果校验失败,以异常的方式抛出
- 节点可以复用,同样的功能可以抽取出来封装到一个节点中
- **流程服务设计元素的表达:**
- 以Json表达Json Schema 定义如下:
```json
{
"type": "object","description": "流程服务设计元素", "required": [ "description","name","nodes","edges"],
"properties":
{
"name": { "type": "string", "description": "流程名称英语描述单词之间使用下划线分割总称不超过32个字符" },
"moduleName": {"type": "string", "description": "模块名称,创建流程时传入,指定流程服务所属的模块"},
"uuid": {"type": "string", "description": "流程服务设计元素的uuid创建流程不传入在更新的时候必须传入"},
"description": {"type": "string", "description": "流程服务描述, 总长度控制在200个字符内"},
"nodes": {
"type": "array", "description": "流程服务的节点列表",
"items": {
"type": "object","description": "流程服务节点对象", "required": ["name", "type", "description"],
"properties": {
"name": {"type": "string", "description": "节点名称, 英语描述单词之间使用下划线分割总称不超过32个字符"},
"type": {"type": "string", "description": "节点 类型,可以是 PROCESS_NODE(顺序节点、SWITCH_NODE(选择节点)、CONDITION_NODE(条件节点),START_NODE(开始节点)"},
"description": {"type": "string", "description": "描述节点的功能, 英语描述单词之间使用下划线分割总称不超过200个字符" }
}
}
},
"edges": {
"type": "array", "description": "流程服务的边列表",
"items": {
"type": "object","description": "流程服务的边", "required": ["fromNode", "toNode"],
"properties": {
"fromNode": {"type": "string", "description": "边的开始节点名称"},
"toNode": {"type": "string", "description": "边的结束节点名称"},
"value": {"type": "boolean", "description": "可选作为条件节点的出边true值表示条件匹配的分支,false表示条件不匹配的分支; 作为循环节点的出边true值表示进入循环false值表示退出循环"}
}
}
}
}
}
```
- 例子:用户注册流程
```
{
"moduleName":"user",//该流程所属的模块
"name":"user_register",//定义该流程的功能
"description":"注册用户",//描述该流程的详细功能
"nodes":[ // 定义该流程包含的节点
{
"name":"start",
"type":"START_NODE",
"description": "起始节点"
},
{
"name":"check_user_registed",
"type":"CONDITION_NODE",
"description":"校验入参合法性;判断昵称是否重名;判断用户是否已注册"
},
{
"name": "create_user",
"descripiton":"创建用户,发送'用户创建'消息, 给用户发送欢迎通知",
"type": "PROCESS_NODE"
},
{
"name":"return_user_info",
"descripiton":"返回用户信息,返回推荐给用户可能感兴趣的好友",
"type":"PROCESS_NODE"
}
],
"edges": [
{
"fromNode": "start",
"toNode":"check_user_registed"
},
{
"fromNode":"check_user_registed",
"toNode":"create_user",
"value":false
},
{
"fromNode":"check_user_registed"
"toNode":"return_user_info",
"value":true
},
{
"fromNode":"create_user",
"toNode":"return_user_info"
}
]
}
```
- **代码产物和修改建议**
- **FlowConfig**
* **生成产物:** 每个模块在service层的生成一个Java类负责注册模块下的所有流程到执行器
* **命名规则:** 类名为${moduleName}FlowConfig
* **职责:** 在应用启动的时候注册模块内的所有的流程服务到执行器
* **类路径:** ```**.service.flow```
- **Service**
* **生成产物:** 在service层的以模块名为类名前缀的${moduleName}FlowService中生成一个流程的入口函数
* **函数命名规则:** 流程名为方法名为后缀pubic void invoke${functionFlowName}(${functionFlowName}Context context)
* **职责:** 在代码逻辑中,使用该流程需要以该函数作为调用入口
* **类路径:** ```**.service```
* **唯一标识符位置:** 其对应的标识符在函数的注解@AutoGenerated中指定, uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|METHOD
- **FlowNode**
* **生成产物:** 在service层生成一个Java类 **注意** 每个FunctionFlow的开始节点(StartNode)不生成
* **类命名规则:** ${nodeName}Node
* **入口函数命名:** pubic void process()
* **职责:** 用于封装内聚性的业务逻辑
* **类路径:** ```service.flow.node.${functionFlowName}```
- **FlowContext**
* **函数命名规则:** {nodeName}Node
* **职责:** 作为流程节点之间的参数传递(包括出参和入参),在实现业务逻辑的时候,按需在这个上下文类中添加所需的字段
* **类路径:** ```**.service.flow.context```
* **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${FunctionFlow在TOCO中的uuid}|FLOW|CONTEXT
- **例子:**
- 用户登录在UserFlowService中生成一个函数invokeLoginFlow该函数通过流程框架根据流程定义调用LoginNodeLoginNode中封装了用户登录的逻辑LoginFlowContext中封装了用户登录的参数和结果。
- **修改建议:** 不修改 service 中的函数, 不修改FlowConfig, 可以修改FlowContext, 添加/修改出入参数, 修改FlowNode中的具体业务逻辑。
#### **2.16 自定义查询**
- 在读方案无法满足需求的情况下,可以使用自定义查询
- 自定查询使用复杂的sql实现业务功能
- 自定义查询的数据访问层使用MyBatis、MyBatisPlus实现
- 自定查询的时候框架不自动生成任何代码(需要模型编写全部代码)
- 各层的代码位置严格遵守章节:**3.2 项目结构与导航**必须有mapper层、service层、必须有DO对象、必须有DTO对象如果是API返回数据必须有VO对象
### **3 生成代码产物补充说明**
- **3.1 支持的语言/框架**
Java、SpringBoot、MyBatis-plus(读)、Hibernate(写)
- **3.2 项目结构与导航**
```
TOCO生成的项目是一个多模块的SpringBoot项目,包括主模块和子模块,它的子模块位于根目录下的/modules目录中**注意** 获取子模块代码文件路径的时候需要从根目录开始所以必须从modules节点开始例如子模块module1下的的java类路径应该是`modules/module1/src/main/java...`
|──main_module
│ ├── common # 项目级别公共的基础模块
│ │ ├──config/ # 中间件配置
│ │ ├──constants/ # 项目级别常量
│ │ ├──enums/ # 项目级别枚举
│ │ ├──redis/ # Redis配置
│ │ ├──response/ # 返回结果封装
│ │ └──utils/ # 项目级别utils
└── └── entrance/ # 项目入口
│ └──AppApplication.java # 项目启动类
└── modules # 子模块列表
└── module1/ # 子模块1
├── common/
│ ├──constants/ # 模块常量
│ ├──utils/ #模块级别utils
│ └──enums/ # 枚举
├── entrance/web/src/main/java/com/{project_name}/{module_name}/entrance/web/
│ ├──controller/ # api 定义
│ ├──converter/ # 把DTO转化成VO
│ ├──vo/ # VO结构定义
│ └── query/ # 把读方案返回的数据转成成VO
│ ├── assembler/ # VO的数据填充
│ ├── collector/ # 读方案返回的id数据展开成完成对象数据
│ └── executor/ # 调用Service的度方案实现同时调用collector和assembler返回最终的VO
├── manager/src/main/java/com/{project_name}/{module_name}/manager/
│ ├── bo/ # 聚合对象定义
│ │ └── base/ # 聚合对象的基类
│ ├── dto/ # 数据传输对象定义
│ ├── converter/ # 复杂Dto非BaseDto)组装
│ ├── facade/ # 调用其他模块的RPC适配器包含RpcAdapter如UserDtoServiceInMeetingRpcAdapter表示从meeting模块调用user模块中方法
│ └── impl/ # Dto的查询接口的实现
├── persist/src/main/java/com/{project_name}/{module_name}/persist/
│ ├── eo/ # 值对象(Eo)的结构定义
│ ├── dos/ # 数据库单表结构的映射
│ ├── qto/ # 读方案的数据库查询实现
│ └── mapper/ # MyBatis的Mapper定义
└── service/src/main/java/com/{project_name}/{module_name}/service/ # BOService包含某个聚合下所有写方案生成的方法、 DtoService包含DTO生成的预定义方法
├── bto/ # 写方案入参的定义
├── converter/ # 对返回的BaseDto按需进行二次扩展
├── query/ # 查询方案的service层入口调用persist层的查询实现
└── base/ # 每个BOService对应的基类
在一个子模块的内它的依赖层级为entrance -> service -> manager -> persist, 同时各个层都依赖 common
```
- **3.3 标准查找流程**
1. **API查找** → `modules/{模块名}/entrance/web/controller/`
2. **DTO查找** → `modules/{模块名}/manager/dto/`
3. **Service查找** → `modules/{模块名}/service/`
4. **数据层查找** → `modules/{模块名}/persist/
- **3.4 特殊注解及含义**
TOC自动生成的类和方法会带有@AutoGenerated注解注解中有2个属性:locked为boolean类型如果locked=true则代表该文件或方法不建议修改;uuid为String类型表示该类或方法的唯一标识如果uuid中包含|字符则说明该uuid为特殊格式由不同类型的数据拼装而成(见**[3.2 设计元素到代码的映射规则及修改建议]**中每种设计元素的代码说明)。
### 4. TOCO 最佳实践
#### 4.1 接口参数类型和返回值选择(Interface Parameter & Return Type Definition):
在做TOCO接口(API、RPC)设计时通常会先判断接口的主要功能是读数据库或写数据库并分析相关的读写方案以及对应的QTO和BTO。如果是读场景则参数会**优先使**用相关的QTO如果是写场景则参数会**优先**使用相关的BTO如果BTO和QTO无法满足要求则可以再增加基本类型或Enum、EO等类型参数。参数类型选择时必须遵循以下要求a.DTO、VO不能作为参数类型b.QTO、BTO不能作为返回值类型。
**重要说明:**
- 本章节描述的是参数选择的**优先级原则**,不是绝对限制
- API参数的绝对限制请参考2.14章节
- "优先使用QTO/BTO"意思是在满足规范的前提下,根据场景选择最合适的参数类型
-----------------------------------------------------------------------------
</TOCO知识库>