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/knowledge.md
2025-10-13 11:13:35 +08:00

1694 lines
104 KiB
Markdown
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平台知识库文档请先仔细阅读并理解其中的概念、规则和最佳实践。在后续的对话中我将为你提供具体的角色定位和任务目标请基于这份知识库来完成相关的设计和开发工作。
<TOCO知识库>
-----------------------------------------------------------------------------
### **1. TOCO 平台概览:**AppointmentBaseDto
- **1.1 平台简介:** TOCO是软件设计和代码自动生成平台。基于DDD、分层架构、CQRS理论覆盖数据库到API的完整开发链路提升开发效率和代码质量
- **1.2 核心价值/目标用户:** 提供软件设计能力,设计成果直接生成标准代码,提高编码一致性和效率
- **1.3 主要特性概览:** 可视化设计、模型关联、多人协作、代码生成器
### **2. TOCO 设计元素:**
#### **2.1 模块 (Module)**
- **定义与用途:** 在TOCO中我们将系统领域细分为具体的模块映射为Java工程中的module。这些模块代表了系统的叶子子域每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性并能提高开发效率和代码质量
- **关键配置:** 名称(小写英文+下划线如meeting,user_detail禁止后缀全局唯一),描述
- **与其他元素关系:** 下列所有设计元素都属于某个模块
- **代码产物:** 生成独立Java Module项目路径/modules/模块名包含entrance、service、manager、persist、common分层
#### **2.2 枚举 (Enum)**
- **定义与用途:** 表示常量值集合,可跨模块使用,可作为字段类型
- **关键属性/配置:** 名称(以_enum结尾全局唯一),枚举值列表(全大写+下划线)
- **与其他元素关系:** 可作为Entity、Dto、Vo、Bto、Qto、Eo的字段类型
- **Enum设计元素的表达:**
- Json格式schema如下
```json
{
"type": "object",
"properties": {
"name": { "type": "string","description": "名称,英语表达下划线分割不超过32字符"},
"uuid": { "type": "string","description": "唯一标识符,创建时为空,更新时必填"},
"description": { "type": "string", "description": "枚举含义和用途不超过128字符"},
"moduleName": { "type": "string", "description": "所属模块,创建时必填,更新时可空"},
"values": {
"type": "array","description": "枚举值列表",
"items": {"type": "string","description": "枚举值英语表达下划线分割不超过32字符"}
}
},
"required":["name","description"]
}
```
* **代码产物和修改建议**
* **生成产物:** common模块中生成Java类
* **职责:** 表达Enum数据结构
* **命名规则:** 类名以Enum结尾
* **类路径:** 位于 <code>**.common.enums</code> 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${Enum的uuid}|ENUM|DEFINITION
- **生成代码:** 在common层生成Enum文件如StatusEnum
- **修改建议:** 不建议修改
#### **2.3 值对象 (EO)**
- **定义与用途:** EO是可复用的POJO数据结构可跨模块使用可作为实体字段类型。
- **关键属性/配置:** 名称以_eo结尾全局唯一。字段类型限制基本类型、List、EO、Enum。如果一个字段对应了某个Enum则应该尽量使用该Enum作为字段类型
- **与其他元素关系:** 可作为Entity、Dto、Vo、Bto、Qto的字段类型支持EO嵌套。
- **EO设计元素的表达**
- 使用Json格式表达Schema定义如下
```json
{
"type":"object",
"properties": {
"name":{ "type": "string", "description": "名称英文下划线分割不超过32字符"},
"description": { "type": "string","description": "描述EO含义和用途不超过128字符"},
"uuid":{ "type": "string", "description": "唯一标识符,创建时为空,更新时必填"},
"moduleName":{ "type": "string", "description": "所属模块,创建时必填,更新时可空"},
"fieldList":{
"type":"array", "description": "EO属性字段列表",
"items":{
"type": "object",
"properties":{
"name": { "type": "string","description": "字段名称英文下划线分割不超过32字符"},
"uuid":{ "type": "string","description": "字段类型为Enum或Eo时必填对应的Enum或Eo标识符" } ,
"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": "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的uuid}|EO|DEFINITION
- **修改建议:** 不建议修改
#### **2.4 实体关系 (ER / Entity)**
- **定义与用途:** 实体对应数据库表,关系为实体间的外键依赖
- **关键属性/配置:** 包含名称、字段、字段类型、主键、索引关系分为1:1和1:N
- **与其他元素关系:** 实体是聚合的基础也是DTO和VO的派生基础
- **代码产物和修改建议**
- 结构定义
* **生成产物**按Mybatis-plus要求生成的Java类
* **职责:** 生成Mybatis-plus结构定义类文件
* **类路径:** 位于 `**.persist.dos` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${Entity的uuid}|ENTITY|DEFINITION
- Mapper
* **生成产物**persist层的Mybatis-plus Mapper类
* **职责:** 为Mybatis-plus框架提供Mapper
* **命名规则**类名以Mapper结尾(${entityName}Mapper)
* **类路径:** 位于 `**.persist.mapper.mybatis` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${Entity的uuid}|ENTITY|MAPPER
- Dao接口
* **生成产物**persist层的Dao接口
* **职责:** 提供Entity数据查询接口为service层提供数据访问入口
* **命名规则**类名以Dao结尾(${entityName}Dao)
* **类路径:** 位于 `**.persist.mapper` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${Entity的uuid}|ENTITY|IDAO
- Dao实现
* **生成产物**persist层的Dao接口实现类
* **职责:** 通过调用Mapper实现Dao接口
* **命名规则**类名以DaoImpl结尾(${entityName}DaoImpl)
* **类路径:** 位于 `**.persist.mapper` 包下
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${Entity的uuid}|ENTITY|DAO
- **修改建议:** 不建议修改
#### **2.5 聚合对象 (BO/业务对象)**
- **定义与用途:** 聚合对象封装一组关联实体。从聚合根实体开始,按层级关系组装其他实体,形成树形结构。提供内存一致性视图和数据操作入口。一个实体只能属于一个聚合对象,聚合对象只能在单一模块中组合。
- **包含元素:** 聚合根实体 + 子实体对象。例如ProductBO包含商品基本信息实体(聚合根)、商品SKU实体、商品库存实体(子对象)。
- **关键配置:** 名称(${EntityName驼峰}BO如StaffBO),聚合根实体,子对象实体。每个聚合必须包含一个聚合根。
- **与其他元素关系:** 聚合是写方案的基础。
- **代码产物和修改建议**
- 综述
- 业务对象包含多个Entity通过嵌套组合表达Entity关系。有子对象时生成BO和BaseBOBaseBO封装实体属性和关系BO用于业务扩展。无子对象时只生成BO类。
- BO
* **生成产物**Manager层生成聚合对象类符合Hibernate标准
* **职责:** 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
* **命名规则**${entityName}BO
* **类路径:** `**.manager.bo`包下
* **唯一标识符位置:** 类注解@AutoGenerated中uuid规则: ${Entity的uuid}|BO|DEFINITION
* **聚合校验:** BO中包含一个validateAggregate(聚合根BO)或valid(非聚合根BO)方法用来编写用于验证业务不变性相关的聚合校验代码。在通过聚合对象操作数据库时TOCO会在框架中自动生成对validateAggregate方法的调用所以禁止在业务代码显式调用以免造成重复校验。
* **修改建议:** 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。校验方法由框架自动调用不需要业务代码显式调用。
- BaseBO
* **生成产物**存在子BO时生成封装不变代码部分
* **职责:** 定义聚合对象,组合成层级结构实现充血模型,支持写链路数据变更、监听变更、数据校验
* **命名规则**${entityName}BO
* **类路径:** `**.manager.bo`包下
* **唯一标识符位置:** 类注解@AutoGenerated中uuid规则: ${Entity的uuid}|BO|DEFINITION
* **修改建议:** 建议修改BO中的validateAggregate或valid方法进行业务不变性校验。不建议修改校验方法以外的代码。
#### **2.6 数据传输对象 (DTO)**
- **定义与用途:** DTO基于某个Entity构建通过外键关系关联多个Entity的数据结构。DTO隐含数据组装逻辑符合外键关系。分为BaseDTO和普通DTOBaseDTO直接派生自Entity包含Entity所有字段每个Entity只有一个BaseDTO普通DTO基于BaseDTO创建包含BaseDTO所有字段可增加扩展字段或自定义字段。DTO不能作为接口参数不能作为HTTP API返回值。
- **创建方式:** 每个Entity自动生成一个BaseDTO命名为${Entity名字}BaseDto如UserBaseDto包含Entity全部字段。其他DTO需手动基于BaseDTO创建。判断是否为BaseDTO是则通过Entity名称获取否则通过DTO要表达的信息创建如会议及议程信息。
- **关键配置:** 名称BaseDTO以BaseDto结尾其他DTO以Dto结尾全局唯一、根Entity、字段列表。字段分三种a.继承Entity或BaseDTO的字段类型相同b.扩展字段包含正向替换和反向注入字段类型为DTO或List<DTO>c.自定义字段类型为基本类型、Eo、Enum、DTO。BaseDTO包含Entity全部字段DTO包含BaseDTO全部字段不裁剪字段可通过外键关系扩展其他Entity无法扩展时可增加自定义字段。
- **字段扩展方式:** DTO通过外键关系替换/注入对应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; //结束时间
}
```
**反向注入:** 选定表如有其他表到该表有外键选择相关的外键属性在选定表中增加以另一表为根的组合对象1:1时或组合对象列表n:1时
需求:"获取会议室和占用它的会议信息"选定MeetingRoom基于Meeting表存在roomid字段为到MeetingRoom的n:1外键。将List<MeetingBaseDto>反向注入到MeetingRoom中
```
MeetingRoomWithMeetingDto {
Long id;// 会议室id
String name;// 会议室名称
List<MeetingBaseDto> meetingList { //反向注入使用该会议室的会议信息
Long id;// 会议id
String title; //会议标题
DateTime startTime; //开始时间
DateTime endTime; //结束时间
}
}
```
"正向替换"和"反向注入"可按需递归调用,组装多个有外键关系的对象。
- **JSON结构描述:** DTO用json结构表示用于理解含义或作为创建、更新DTO工具的参数。字段含义dto的uuid为唯一标识创建时设置为null复用时填入uuid。expandList为正向替换reverseExpandList为反向注入customFieldList为自定义字段。expandList中foreignKeyInThisEntity为本表外键字段名dtoFieldName为替换后字段名reverseExpandList中foreignKeyInOtherEntity为他表外键字段名dtoFieldName为注入后字段名customFieldList中uuid为自定义字段UUID创建时不填入更新时需传入用于定位typeUuid为类结构UUIDtype为Enum、Eo时包含innerType为List内部类型type为List时包含innerUuid为List内部类结构UUIDtype为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_room",
"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。
- **预定义方法:** 每个DTO基于根Entity的唯一索引自动生成预定义RPC方法及实现预定义方法获取根Entity数据通过RPC自动获取所有扩展字段数据并拼装。如实体user有唯一索引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
判断使用方式的步骤:
- 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)**
- **定义与用途:** VO基于BaseDTO或指定的其他DTO派生通过外键关系关联多个BaseDTO的数据结构。用于视图层与前端数据传输作为HTTP API返回值或读方案返回值。不能作为接口参数或RPC返回值。
- **关键配置:** 名称以Vo结尾全局唯一、根Entity、所属模块、派生源、字段列表。字段分三种a.继承DTO字段基础类型/EO/Enum保持类型不变DTO类型转为对应VO类型b.扩展字段正向替换和反向注入类型为VO或List<VO>c.自定义字段基本类型或VO类型。可裁剪无用字段可通过外键关系扩展其他BaseDto。无法扩展时可增加自定义字段。
- **继承字段类型转换:** VO只与派生源VO有转换关系。当VO派生自某DTO且继承了派生源DTO中的DTO类型字段假设DTO-A则VO中对应字段类型必须为VO假设VO-A且**VO-A必须派生自DTO-A**,以维持转换关系。
- **与DTO的区别:** DTO用于服务层传输通常作RPC返回值与数据模型更近复用性强VO用于视图层传输通常作API返回值与UI展示更近可裁剪冗余字段复用性弱。
- **VO所属模块:** VO无法被跨模块引用模块A内部的API只能使用模块A内部的VO。VO可以与其派生源DTO不在同一模块meeting模块的API需要返回meeting_room_dto派生出的VO则需要在meeting模块创建派生自meeting_room_dto的VO以便被meeting模块的API使用注意创建VO时传入的moduleName字段,用于指定VO所在的模块。
- **创建方式:** VO通常基于某个BaseDTO及外键关系派生也可直接创建与DTO无关的全自定义字段VO尽量少用仅用于特殊页面组装无关数据的场景
- **根VO和子VO:** VO分两种1.根VO最外层VO结构需通过TOCO创建有uuid唯一标识可被其他根VO或子VO引用2.子VO根VO内部嵌套VO通过外键关系关联BaseDTO后自动创建仅附属于一个根VO无uuid。需描述VO字段和扩展关系通过**创建根VO**使TOCO**自动创建**子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关系。多个会议占用同一个会议室。
通过"正向替换"组装出:
```
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 //会议结束时间
}
}
```
- **JSON结构描述:** VO用json结构表示用于理解含义或作为创建、更新VO工具参数。字段含义expandList为正向替换reverseExpandList为反向注入extendFieldList为来自派生源DTO的字段customFieldList为自定义字段。expandList中foreignKeyInThisEntity为本表外键字段名voFieldName为替换后字段名reverseExpandList中foreignKeyInOtherEntity为他表外键字段名voFieldName为注入后字段名customFieldList中uuid为自定义字段UUID创建时不填更新时需传入定位typeUuid为类结构UUIDtype为List且innerType=Enum、Eo时包含extendFieldList中name为派生源dto中的字段名若DTO字段非DTO类型则VO字段类型与DTO一致若DTO字段为DTO或List<DTO>类型由于VO字段不能为DTO**必须**将DTO转换为VO所以extendFieldList会有**vo结构**表示该字段DTO类型派生出的VO定义注意该VO字段中的VO类型必须派生自继承的DTO字段类型
示例系统中存在meeting_detail_dto
```
{
"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字段
```
{
"vo": {
"uuid": null,
"name": "metting_with_room_vo",
"description": "会议详情,包含会议室信息,以及会议室禁用列表",
"rootVo": "metting_with_room_vo",
"moduleName": "meeting",
"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无根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)**
- **定义与用途:** WO基于Entity通过外键关系关联多个Entity的数据结构。WO表达数据取数拼装逻辑符合外键关系。WO作为ReadPlan查询上下文创建ReadPlan前需先创建WO对象。理解ReadPlan语义时需要WO作为上下文。
当返回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或从DTO派生出的VO结构构建同构WO对象扩展和反向扩展字段名保持一致根据查询需求和过滤需求对WO二次裁剪或扩展
- 去掉过滤和查询都不需要的扩展
- 补全查询或字段过滤需要的扩展
- **关键配置:** WO字段分三种a.继承Entity字段类型与Entity相同b.扩展字段含正向替换和反向注入字段类型为WO或List<WO>
- **字段扩展方式:** WO通过外键关系替换/注入对应Entity信息对象化表达有外键关系的Entity信息。存在外键关系且满足条件即可扩展a.正向替换当前实体有指向其他实体的外键字段b.反向注入:其他实体有指向当前实体的外键字段。
- 禁止从无根VO创建WO
例如两个Entity
```java
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; //会议结束时间
}
```
**反向注入**:选定表,其他表到该表有外键,选择需求相关的外键属性在选定表中增加以另一表为根的组合对象(1:1关系时)或组合对象列表(n:1关系时)。
需求:"获取会议室和占用它的会议信息"选定MeetingRoom基于Meeting表存在roomid字段为到MeetingRoom的n:1关系外键。将List<MeetingBaseDto>反向注入到MeetingRoom中
```
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)**
- **定义与用途:** 读方案描述如何从数据库获取DTO和VO列表数据提供三个能力
- 根据查询条件返回符合条件的DTO或VO的id列表
- 根据查询条件返回符合条件的DTO或VO的id数量
- 根据字段过滤条件对DTO和VO的列表字段数据进行过滤
- **注意** 不派生自DTO的VO不能创建读方案
- **读方案能力边界**
- 支持sql的count、exists、left join、not、in、like、between、and、or、括号、order by、limit、offset
- 不支持group by、having语法不支持其他函数
- 1:N外键关系只能使用exists1:1外键关系可使用left join和exists
例如实体t和t1t1有t_id外键指向t若为1:N关系则`select * from t where exists (select * from t1 where t.id=t1.id)`合法,`select * from t left join t1 on t.id=t1.id`非法
- exists只能选择后表字段left join可使用前表和后表字段
例如实体t(id,name)实体t1(id,name,t_id)`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 ?`合法
- 需求超出读方案能力时,必须使用自定义查询
- **特别注意** 根据主键获取单个或批量DTO/VO且不需过滤列表字段数据的需求必须使用DTO自动生成的预定义方法禁止使用读方案
- **关键配置:** 名称(小写字母+下划线不以read_plan结尾全局唯一)、返回结构(DTO/VO一个读方案只能返回一种类型)、查询条件自然语言描述、是否生成计数方法、排序字段、过滤字段及过滤条件
- **与RPC、代码的关系:** 返回DTO的读方案自动生成RPC方法参数为QTO返回值为DTO列表返回VO的读方案自动生成Java方法参数为QTO返回值为VO列表方法内部逻辑由TOCO完全实现
- **生成的读方案RPC使用:** 读方案RPC属于对应模块其他模块需先订阅该RPC用adapter调用当前模块直接调用对应Service
- **创建方式:** 先创建或选择一种DTO或VO作为返回值类型然后定义查询条件自然语言描述、分页方式、是否生成计数方法、排序字段等
- **排序** 支持两种方式:
- **默认排序**:指定默认排序字段(不需入参指定排序字段)
- **自定义排序**:指定排序字段(需入参指定排序字段),满足列表头动态排序需求
- **排序字段来源**只能来源自根WO(不包括列表WO属性)以及根WO扩展出的非列表属性的WO字段。排序字段是从根节点到当前属性的路径例如ADto包含BDto bDtoBDto有name属性排序字段路径为bDto.name
- 提取需求中的查询信息,以输入的查询对象作为上下文,构建查询语句
- 如有列表属性过滤需求:提取需求中的过滤信息,针对可过滤字段(列表属性)分别创建过滤条件。过滤条件不能使用列表属性作为查询条件属性(不能使用contains语法)。属性字段是从根节点属性到当前属性的路径
- **查询语法和过滤语法**
- 基本语法格式:属性名 操作符 变量或常量,变量格式为#变量名;枚举类型常量用单引号包围,例如:'MALE'
- 数值、时间类型属性操作符:!=, ==, >, <, <=, >=, in, notIn, isNullOrNot
- 文本类型属性操作符like, isNotNull, isNull, !=, ==, in, notIn, isNullOrNot
- isNullOrNot是三元操作符根据入参判断过滤某值是否为null。例如gender isNullOrNot #genderIsNullOrNot若入参为true等价于where gender is null若为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 定义了如下语法
<code>
@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
</code>
- **读方案设计元素的表达**
- 以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"]
}
}
```
- **举例**
- 上下文
<code>
public class meeting_room {
storey storey; // 楼层对象
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; // 位置对象
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
}
}
</code>
- 用户需求
获取一段时间未使用的会议室(包含当前会议已选择的会议室),根据会议标题过滤会议信息,分页返回。
- 读方案定义
<code>
{
"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"
}
]
}
</code>
- **代码产物和修改建议**
- **QTO查询参数对象**
* **生成产物:** Service层生成Java类
* **命名规则:** 类名以Qto结尾(${ReadPlanName}Qto)
* **类路径:** `**.persist.qto`包下
* **职责:** 读方案的查询参数结构可作为API参数直接透传给RPC调用
* **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${ReadPlan的uuid}|QTO|DEFINITION
* **内部结构:** QTO包含分页相关字段:
<code>
private String scrollId; //瀑布流游标
private Integer size; //每页记录数
private Integer from; //开始记录位置
</code>
- **Dao**
- **生成产物:** Dao层生成Java类
- **命名规则:** 类名以Dao结尾(${ReadPlanName}Dao)
- **类路径:** `**.persist.mapper`包下
- **职责:** 读方案对应的数据库查询方法
- **唯一标识符位置:** 类注解@AutoGenerated中指定uuid规则: ${ReadPlan的uuid}|QTO|DAO
- **QueryExecutor**
- **生成规则**
- 只有当读方案明确指定返回VO时才生成QueryExecutor
- QueryExecutor的命名规则${VoName}QueryExecutor
- 如果读方案返回DTO则不生成QueryExecutor需要通过DtoQueryService + VoConverter组合实现
- **生成产物** 返回**VO**的查询方案在entrance层生成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结构则会生成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代码结构:
<code>
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; //瀑布流查询是否有下一页
}
</code>
- **例子:**
* 根据用户名称查询用户列表返回UserDTO生成UserNameQto、UserNameQtoService、UserNameQtoDao、UserNameQueryService; UserNameQueryService调用UserNameQtoServiceUserNameQtoService调用UserNameQtoDao
- **修改建议:**
- 如有结果数据二次处理建议在QueryService和QueryExecutor中扩展代码不建议修改QTO文件
#### **2.10 查询传输对象(QTO)**
- **定义与用途:** QTO是读方案的查询参数结构。每个读方案对应一个QTO。调用方使用QTO结构传入查询字段值完成数据库查询
- **如何创建/生成:** 创建读方案后TOCO自动生成对应的QTO。无需单独创建
- **关键配置:** 名称(${ReadPlanNameQto}驼峰命名查询字段列表如idIsnameLikeschoolNameLike等
- **与API的关系:** QTO可作为API参数。API接收参数后直接透传给内部RPC调用。QTO只用于读操作参数**禁止用于写参数结构**
#### **2.11 写方案 (WritePlan单聚合操作)**
- **定义与用途:** 写方案是数据库写操作的唯一方式每个写方案只能变更一个聚合的数据。写方案可以一次操作聚合内的多张表。例如location(父对象/聚合根)和storey(子对象)是1:N关系可创建写方案同时更新location信息和操作storey列表(新增/删除/修改)也可创建写方案单独更新单个storey对象。
- **关键配置:** 名称(小写+下划线不以write_plan结尾全局唯一),操作的聚合,聚合内的实体和字段,每个实体的操作类型:
- CREATE创建单个实体
- UPDATE更新单个实体
- DELETE删除单个实体
- CREATE_ON_DUPLICATE_UPDATE创建或更新单个实体
- FULL_MERGE批量更新列表数据执行CREATE_ON_DUPLICATE_UPDATE操作删除不在传入列表中的旧数据
- PARTIAL_MERGE批量更新列表数据执行CREATE_ON_DUPLICATE_UPDATE操作
- **与RPC关系** 每个写方案自动生成一个RPC方法参数为对应BTO返回值为聚合根实体主键值只实现当前聚合的数据库操作。
- **生成RPC使用** 写方案RPC属于所属模块其他模块需先订阅该RPC用adapter调用当前模块直接调用对应Service。
- **与聚合关系:** 每个聚合可定义多个写方案,但每个写方案只能操作一个聚合内的表。
- **创建方式:** 先选定聚合和要操作的实体,再确定每个实体的操作类型
- 提取需求中的更新需求,根据上下文构建写方案
- 输入备选聚合对象定义,每个聚合定义多个实体的嵌套关系
- 明确聚合内实体的父子层级关系
- 提取需求相关的变更名称和描述
- 选择最合适的聚合对象(最多只能选一个)
- 按对象粒度选出需变更对象
- 选定变更对象的字段(每个对象至少选一个字段,无法确定时默认选全部,字段必须明确指定名称)
- 为每个对象设置操作类型
- 列表属性可批量操作:
- 批量更新列表(FULL_MERGE/PARTIAL_MERGE):需包含父对象
- 批量删除列表需包含父对象父对象设为UPDATE
- 批量创建列表需包含父对象父对象设为UPDATE或CREATE
- 示例meeting对象有List<meeting_agenda> agendaList批量创建agenda需同时设置meeting和meeting_agenda对象meeting_agenda设为CREATE只设置meeting_agenda的CREATE则一次只能创建一条数据
- FULL_MERGE和PARTIAL_MERGE只用于列表属性单值属性用CREATE_ON_DUPLICATE_UPDATE
- UPDATE/DELETE/CREATE_ON_DUPLICATE_UPDATE/FULL_MERGE/PARTIAL_MERGE操作必须指定一个唯一键(包括主键)用于确定数据记录主键字段也要包含在fields中
- 唯一键:实体中的唯一索引,由一个或多个字段组成
- 选定对象层级不能跳跃,发生不连续时去掉不连续的叶子对象
- 父对象和子对象操作类型限制:
- 父对象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/Integer类型字段可设为增量更新设置在incrFields中配合UPDATE/CREATE_ON_DUPLICATE_UPDATE/PARTIAL_MERGE/FULL_MERGE使用。例如A实体count字段设为增量更新效果是A.count = A.count + ?,传入负值可减值
- 字段选择:
- 按需选择字段
- 当CREATE操作的时候如果主键不又入参指定不勾选主键字段主键字段的值由框架分配如果勾选了主键但是不传递参数值会造成运行时异常
- 自定义字段除了来自实体的字段外还可以添加自定字段对生成物Bto对象进行扩展
- **写方案设计元素表达:**
```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": "操作字段列表"
},
"incrFields": {
"type": "array",
"items": {
"type": "string",
"description": "增量字段名称"
},
"description": "增量操作字段列表"
},
"customField": {
"type": "array",
"items": {
"type": "object",
"description": "自定义字段名称",
"properties": {
"name": {
"type": "string",
"description": "字段名称英文下划线分割不超过32字符"
},
"uuid": {
"type": "string",
"description": "字段类型为Enum或Eo时必填对应的Enum或Eo标识符"
},
"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": "List元素类型String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum,BigDecimal"
}
},
"required": [
"name",
"type"
]
},
"description": "自定义字段列表"
},
"required": [
"bo",
"action",
"fields"
]
}
}
}
},
"required": [
"name",
"description",
"operations",
"bo"
]
}
```
- **示例:**
- **上下文:**
```java
public class meeting {//会议聚合
//唯一键:(id)、(meeting_name)
Long 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函数完成聚合对象操作实现数据库写操作
* **返回值:** 聚合根实体记录主键值
* **类路径:** `**.service`
* **唯一标识:** 函数注解@AutoGenerated中指定uuid规则${WritePlan的uuid}
- **BaseBOService**
- **生成产物:** 在聚合BaseBOService中生成系列函数根据入参完成聚合对象变更
- **职责:** 为每个实体生成增删改函数,根据参数结构和聚合结构构建嵌套调用逻辑,完成聚合对象变更,记录并返回变更情况
- **类路径:** `**.service.base`
- **禁止:** 修改该类
- **BTO (业务变更传输对象)**
* **生成产物:** Java类以内部类方式表示层级结构注意BTO只能由写方案生成不能凭空创建
* **命名规则:** 以Bto结尾(${WritePlanName}Bto驼峰命名)
* **禁止:** 修改该类
* **类路径:** `**.service.bto`包下
* **职责:** 写方案的参数结构每个写方案对应一个BTO调用方按BTO结构向写方案RPC方法传入实体字段值完成数据库变更
* **唯一标识:** 类注解@AutoGenerated中指定uuid规则${WritePlan的uuid}|BTO|DEFINITION
- **示例:**
- 创建用户及设置的写方案create_user_and_setting生成CreateUserAndSettingBto在UserBOService中生成函数createUserAndSetting该函数调用BaseUserBOService中生成的createUserAndSetting其中BaseUserBOService还生成createUser和createSetting函数一起完成用户创建和设置创建逻辑。
- **修改建议:**
- 不能修改BaseBOService函数不建议修改BTO文件。建议在BOService中扩展代码处理可能被复用的前后逻辑如数据库前后值对比、常被复用的校验逻辑(业务不变性校验除外)、需要在一个事务内执行的其他写操作等。
- 父类函数返回BoResult类记录各Bto和Bo实例对应关系及Bto实例操作结果可通过以下接口获取。例如创建用户的写方案中BoService入参为CreateUserBto用户id由数据库生成需要返回创建用户id时通过boResult.getAddedResult(createUserBto).getBo().getId()返回新建用户id
```java
/**
* 获取更新成功的bto结果
*/
public UpdatedBto getUpdatedResult(final Object bto);
/**
* 获取成功创建的Bto结果
*/
public AddedBto getAddedResult(final Object btoObj);
/**
* 获取Bto对应的删除结果
*/
public DeletedBto getDeletedResult(final Object btoObj);
```
```java
//记录Bto和它对应的更新的实体的前值以及更新后的Bo的
@Setter
@Getter
public class UpdatedBto<Bto,Entity,BO> {
private Bto bto;//Bto入参
private Entity entity;//Bto对应的Entity更新前值
private BO bo;//bo更新后的值
}
```
```java
//记录Bto和它对应删除的实体
@Setter
@Getter
public class DeletedBto<Bto,Entity> {
private Bto bto; //Bto入参
private Entity entity; //Bto对应的Entity前项
}
```
```java
//记录Bto和它对应创建出的Bo
@Setter
@Getter
public class AddedBto<Bto,BO> {
private Bto bto;//Bto入参
private BO bo;//根据bto创建出来的Bo
}
```
#### **2.12 业务变更传输对象(BTO)**
- **定义与用途:** BTO是写方案自动生成的参数结构每个写方案生成一个BTO。BTO将操作实体按关系组成树形集合聚合根在最外层。调用方按BTO结构传入实体字段值完成数据库写操作
- **创建方式:** BTO只能由写方案自动创建不能单独新建。创建写方案后TOCO自动生成对应BTO作为参数结构
- **关键配置:** 名称(${WritePlanName}Bto驼峰命名嵌套树形实体和字段列表字段全部来自Entity。示例
```java
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参数再增加其他基本类型参数或EO
b. 用基本类型参数或EO作参数在接口流程中将参数转换为各写方案生成的BTO再调用多个写方案
c. 流程过于复杂时优先考虑使用基本类型参数或EO
#### **2.13 服务层方法 (RPC)**
- **定义与用途:** TOCO中RPC是服务层方法。分两种公开RPC可被其他模块订阅并通过适配器调用非公开RPC只能当前模块调用。非公开RPC可变为公开供其他模块使用
- **创建/生成方式:** RPC有4种创建方式a.创建DTO后自动生成RPC公开性与DTO一致b.返回DTO的读方案根据分页和计数配置自动生成非公开RPCc.写方案创建后自动生成非公开RPC返回聚合根实体主键值d.需求不满足时通过TOCO创建自定义RPC需指定参数、返回值、公开性
- **优先复用:** 用户需要创建RPC时有明确要求按要求创建。无明确要求时先判断能否通过读方案、写方案、DTO自动创建RPC最后考虑自定义RPC
- **自定义RPC和手写方法关系:** 两者都可手动实现服务层方法。区别需要被其他模块订阅用自定义RPC只是API私有调用不需要外部开放用手写方法
- **关键配置:** 类名(驼峰首字母大写以Service结尾)、是否公开、方法名(驼峰,首字母小写)、请求参数、返回值。分页查询参数为Qto类型时Qto已包含from、size、scrollId属性无需额外参数
- **TOCO中RPC存储:** RPC在TOCO中只存储方法签名不存储执行逻辑。了解实现逻辑需阅读RPC对应代码
- **参数类型:** RPC参数**只能**为QTO、BTO、Enum、EO、基本类型可为单值或列表。对象类型优先用QTO、BTO**禁用**VO和自定义结构如Object
- **返回值类型:** RPC返回值**只能**为DTO、Enum、Eo、基本类型可为单值或列表**禁用**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为返回结构参数和response结构相同其中name为参数名type为参数类型取值Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,Dto,Qto,Bto,List,PageResult,Void参数不能为Void和PageResult无返回值设为Void分页查询结果设为PageResult且innerType必为Dto对应VSQueryResult<XxxDto>description为描述typeUuid为类结构UUIDtype为Enum、Eo、Dto时传入对象uuidQto时传入读方案uuidBto时传入写方案uuidinnerType为List内部类型type为List或PageResult时包含innerUuid为List内部类结构UUIDtype为List或PageResult且innerType为Enum、Eo、Dto时传入对象uuidinnerType为Qto时传入读方案uuidBto时传入写方案uuid
- **生成代码:** RPC在service层生成类文件和实现函数包含DTO自动生成的RPC如UserDtoService.getById、读写方案自动生成的RPC如UserDtoQueryService.queryByListQto、UserBOService.createUser、自定义RPC如UserCustomService.customMethod。公开RPC才能被其他模块使用订阅后生成RpcAdapter适配器其他模块通过RpcAdapter调用。RpcAdapter的命名方式严格遵守以下格式${ClassName}In${ModuleName}RpcAdapter以下为示例Order模块订阅User模块的UserDtoService.getById在Order模块生成UserDtoServiceInOrderRpcAdapter.getById方法Order模块代码必须通过@Resource private UserDtoServiceInOrderRpcAdapter userDtoServiceInOrderRpcAdapter;注入适配器后调用。**必须注意**:变量命名必须是类名首字母小写,禁用其他变量名
- **修改建议:** 建议修改RPC方法不建议修改RPC方法签名、适配器内容
#### **2.14 应用程序接口 (API)**
- **定义与用途:** API定义对外HTTP接口
- **创建方式:** 通过TOCO创建需指定参数和返回值
- **存储特点:** TOCO只存储URI和方法签名不存储执行逻辑。了解实现需阅读对应代码
- **关键配置:** uri(/api/${moduleName}/xxx如/api/user/create全局唯一)、类名(以Controller结尾)、方法名(驼峰小写)、请求参数、返回值。分页查询参数为Qto时已包含from、size、scrollId属性无需额外参数
- **参数类型:** 只能为QTO、BTO、EO、Enum、基本类型可为单值或列表。对象类型优先用QTO、BTO。禁用DTO和自定义结构
- **返回值类型:** 脚手架自动包装返回值(code、message、data)。只能为VO、Enum、EO、基本类型可为单值或列表。禁用DTO、QTO、BTO、自定义结构。对象类型优先用VO
- **json结构:** 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为返回结构。参数和response结构相同name为参数名type为参数类型(Boolean,String,Integer,Long,Float,Double,BigDecimal,Date,ByteArray,Enum,Eo,List,PageResult,Vo,Qto,Bto,Void参数不能为Void和PageResult无返回值设为Void分页结果设为PageResult且innerType必为Vo对应VSQueryResult<XxxVo>)description为描述typeUuid为类结构UUID(type为Enum、Eo、Vo时传入对象uuidQto时传入读方案uuidBto时传入写方案uuid)innerType为List内部类型(type为List时包含)innerUuid为List或PageResult内部类结构UUID(type为List或PageResult且innerType为Enum、Eo、Vo时传入对象uuidinnerType为Qto时传入读方案uuidBto时传入写方案uuid)
- **代码产物和修改建议**
- **生成代码:** entrance层生成Controller及对应API方法
- **修改建议:** 建议修改API方法实现内容禁止直接修改API方法签名、URI。修改API定义需通过修改API设计元素实现
#### **2.15 流程服务FunctionFlow)**
- **定义与用途:** TOCO将复杂业务拆解为流程节点基于业务逻辑内聚性合并功能构建工作流式逻辑流程提升代码可维护性。内嵌流程引擎执行生成代码。
- **使用场景:**
- API/RPC涉及写服务超过3个时**必须**使用
- 用户要求使用时
- **节点封装:**
- 以数据内聚为首要考虑,一个节点围绕一个核心写服务,包含读服务、数据处理转换、附属功能。除条件节点、入参校验、数据返回节点外,每节点至少包含一个写服务。
- **配置:** 名称(小写字母+下划线),拆解复杂业务逻辑。简单业务流程无需使用。
- **创建方式:**
- 流程非逻辑伪代码,不表达全部细节。相关内聚、相似功能封装到一个节点内。如用户注册:创建账号、用户信息、发送通知属于创建用户相关信息,无逻辑分叉,可内聚在一个节点。
- 节点间参数和返回值通过统一上下文Context传递(编码时按需修改Context文件添加字段)。
- 用JSON表达逻辑流。节点类型顺序节点、条件节点、选择节点、开始节点。节点间通过有向边连接表示执行方向。从开始节点可达各节点。
- 边定义:顺序节点多条入边多条出边(一条出边为下个执行节点,多条为并发执行分支)条件节点多条入边2条出边(True/False分支);选择节点多条入边多条出边(每条为选择路径);开始节点一条出边无入边。
- 条件节点只封装判断逻辑返回TRUE/FALSE循环节点只封装判断逻辑返回TRUE/FALSE选择节点只封装分支选择逻辑返回下游分支节点名称。
- 终止流程:直接抛异常。
- 校验逻辑放在顺序节点中,失败则抛异常。
- 节点可复用,相同功能可抽取封装到一个节点。
- **表达方式:**
- 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退出循环"}
}
}
}
}
}
```
- 示例:用户注册流程
```json-with-comments
{
"moduleName":"user", // 该流程所属的模块
"name":"user_register", // 定义功能
"description":"注册用户", // 详细功能描述
"nodes": [ // 包含节点
{
"name":"start",
"type":"START_NODE",
"description": "起始节点"
},
{
"name":"check_user_registed",
"type":"CONDITION_NODE",
"description":"校验入参合法性;判断昵称是否重名;判断用户是否已注册"
},
{
"name": "create_user",
"description":"创建用户,发送'用户创建'消息, 给用户发送欢迎通知",
"type": "PROCESS_NODE"
},
{
"name":"return_user_info",
"description":"返回用户信息,返回推荐给用户可能感兴趣的好友",
"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中生成流程入口函数
* **命名:** `public void invoke${functionFlowName}(${functionFlowName}Context context)`
* **职责:** 代码逻辑中使用该流程的调用入口
* **路径:** `**.service`
* **标识:** 函数注解@AutoGenerateduuid规则${FunctionFlow的uuid}|FLOW|METHOD
- **FlowNode**
* **生成产物:** service层生成Java类**注意**开始节点(StartNode)不生成
* **命名:** ${nodeName}Node
* **入口函数:** `public void process()`
* **职责:** 封装内聚性业务逻辑
* **路径:** `service.flow.node.${functionFlowName}`
- **FlowContext**
* **命名:** ${functionFlowName}Context
* **职责:** 流程节点间参数传递(出参和入参),实现业务逻辑时按需在此类中添加字段
* **路径:** `**.service.flow.context`
* **标识:** 类注解@AutoGenerateduuid规则${FunctionFlow的uuid}|FLOW|CONTEXT
- **示例:**
- 用户登录UserFlowService中生成invokeLoginFlow函数通过流程框架根据流程定义调用LoginNodeLoginNode封装用户登录逻辑LoginFlowContext封装登录参数和结果。
- **修改建议:** 不修改service函数和FlowConfig可修改FlowContext添加/修改出入参数修改FlowNode中具体业务逻辑。
#### **2.16 自定义查询**
- 读方案无法满足需求时,使用自定义查询
- 自定义查询使用复杂sql实现业务功能
- 数据访问层使用MyBatis、MyBatisPlus实现
- 自定义查询时框架不自动生成代码(需要手动编写全部代码)
- 各层代码位置严格遵守**3.2 项目结构与导航**必须有mapper层、service层、DO对象、DTO对象API返回数据必须有VO对象
#### **2.17 领域消息DomainMessage)**
- **定义与用途:** TOCO支持领域消息通过创建和订阅领域消息可以监听聚合对象的状态变化创建、删除、更新)TOCO会自动生成消息的发送逻辑
- **关键配置:** 名称(小写字母+下划线), 由于消息是全局可见所以TOCO限制了名称项目级别不能重复; **注意** 在监听状态变化的时候不要命名为某个字段的变更,比如 order_payed, 也不要按照业务语义命名例如order_payed_complete,因为实际上是order表的任意变更都会受到消息因此命名为order_changed或order_updated更准确的表达了消息的触发语义 同理对于create事件一般命名为 xxx_created对于delete时间一般命名为 xxx_deleted
- **与聚合的关系:** 每个聚合下可以定义多个领域消息,每次可以监听聚合下面其中一个实体的状态变更
- **如何创建/生成:** 创建领域消息,需要先确定聚合,然后确定需要监听的实体以及监听的变更类型
- **最佳实践** 如果只是字段区别,应该修改现有的领域消息,增加相应的字段即可,而不是新创建一个领域消息;
- **领域消息的定义表达**
- 以Json表达Json Schema 定义如下:
```json
{
"type": "object", "required": ["name","description", "bo", "entity", "action","fields"],
"properties": {
"name": {"type": "string", "description": "消息名称,单词之间使用下划线分割总称不超过32个字符,一个模块的领域消息名称不能重复"},
"description": {"type": "string", "description": "消息描述不超过128个字符"},
"bo": {"type": "string", "description": "聚合名称"},
"entity": {"type": "string", "description": "监听的实体名称,必须归属于聚合"},
"fields": {"type": "array", "description": "指定消息里返回的entity的字段对应的字段名称","items": {"type": "string", "description": "字段名称"}},
"action": {"type": "string", "description": "监听的变更类型可选值create、update、delete"},
"delayInSeconds": {"type": "integer", "description": "延迟时间,单位为秒, 如果希望消息延迟消费,则设置该字段, 例如: 订单下单后5分钟未支付触发一个消息则可以监听订单的创建状态并且设置延迟时间5分钟这样消息会在5分钟后被消费"},
"uuid": {"type": "string", "description": "消息的uuid在更新领域消息时传入创建的时候不传"}
}
}
```
- **代码产物和修改建议**
- **发送逻辑** 会在相应聚合的实体中通过hibernate的监听器中实现消息的发生逻辑**不能修改**
- **生成产物:**
- **消息载体:** 在manager层生成一个Java类封装消息体
- **命名规则:** 类名为${domainMessageName}Mo
- **职责:** 消息内容载体
- **类路径:** ```**.manager.mo```
- **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${DomainMessage在TOCO中的uuid}|DMO|DEFINITION
- **例子**
用户聚合包含了user实体, 用户实体有 user_id user_name 字段那么创建一个领域消息名称为user_created描述为用户创建成功聚合名称为user实体名称为user监听的变更类型为create返回的字段为user_iduser_name。则会生一个类<code> UserCreatedMo {Long userId;String userName;}</code>特别的对于update的监听生成消息里会包含字段的前值命名以old结尾, 例如:<code>UserUpdatedMo {String userName ; String userNameOld;}</code>
#### **2.18 普通消息 CommonMessage)**
- **定义与用途:** TOCO支持普通消息普通消息可以自由定义消息内容。定义消息后TOCO可以生成消息体以及消息的发送模板只需调用这个发送函数即可发送消息; 消息队列的创建配置等工作TOCO自动完成
- **关键配置:** 名称(小写字母+下划线), 由于消息是全局可见所以TOCO限制了名称项目级别不能重复;
- **普通消息的定义表达**
- 以Json表达Json Schema 定义如下:
```json
{
"type":"object",
"properties": {
"name":{ "type": "string", "description": "消息名称,单词之间使用下划线分割总称不超过32个字符,一个模块的领域消息名称不能重复"},
"description": { "type": "string","description": "消息描述不超过128个字符"},
"uuid":{ "type": "string", "description": "唯一标识符,创建时为空,更新时必填"},
"moduleName":{ "type": "string", "description": "所属模块,创建时必填,更新时可空"},
"delay": {"type": "boolean","description": "是否是延迟消息,如果是延时消息,在生成的的发送消息函数里添加延迟时间参数"},
"transactional": {"type": "boolean","description": "是否是事务消息,如果是事务消息,消息的发送和外层事务保持一致"},
"fieldList":{
"type":"array", "description": "消息属性列表",
"items":{
"type": "object",
"properties":{
"name": { "type": "string","description": "字段名称英文下划线分割不超过32字符"},
"uuid":{ "type": "string","description": "字段类型为Enum或Eo时必填对应的Enum或Eo标识符" } ,
"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": "List元素类型String,Integer,Long,Float,Double,Boolean,Date,Eo,Enum,BigDecimal"}
},
"required":[ "name","type"]
}
}
},
"required":["name","description"]
}
```
**代码产物和修改建议**
- **消息载体类:** 在manager层生成一个Java类封装消息体
- **命名规则:** 类名为${commonMessageName}Mo
- **职责:** 消息内容载体
- **类路径:** ```**.manager.mo```
- **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${CommonMessage在TOCO中的uuid}|MO|DEFINITION
- **发送类** 在service层生成一个Java类封装消息发送逻辑
- **命名规则:** 类型名为 ${commonMessageName}MoMessageSender
- **职责** 封装发送函数,作为消息发送的入口
- **类路径:** ```**.service.mq.producer```
- **唯一标识符位置:** 其对应的标识符在类注解@AutoGenerated中指定, uuid规则: ${CommonMessage在TOCO中的uuid}|MO|PRODUCER
- **例子**
- 在用户注册成功后需要给用户发一封邮件可以定义个普通消息名称为user_registered, 包含了字段: long user_id, String nick_name 生成代码后,会生成 类:<code> UserRegisteredMo {Long userId;String nickName;}</code> 和一个发送类 <code>UserRegisteredMoMessageSender {void send(UserRegisteredMo message);}</code>
#### **2.19 订阅消息 (SubscribeMessage)**
- **定义与用途:** TOCO支持订阅消息普通消息和领域消息, 订阅消息后生成代码会生成消息消费的模板代码, 后续只需在对应的模板代码里填写业务逻辑,而无需关注如何订阅消息的代码逻辑 **注意**在TOCO中消息订阅按照模块独立订阅一个消息可以被多个模块订阅同一个消息在一个模块中只能订阅一次
- **订阅消息的定义表达**
- 以Json表达Json Schema 定义如下: 通过msgId或者msgName指定订阅的消息
```json
{
"type": "object", "required": ["moduleName"],
"msgId": {"type": "string", "description": "消息id"},
"msgName": {"type": "string", "description": "消息名称"},
"moduleName": {"type": "string", "description": "模块名称,指定消息被订阅到哪个模块"}
}
```
- **代码产物和修改建议**
- **Consumer类**
- **命名规则:** 在指定模块的的service层生成一个Java类封装了消息的消费类入口
- **命名规则:** 类名为${messageName}Consumer
- **职责:** 消息的消费类入口
- **类路径:** ```**.service.mq.consumer```
- **MsgHandler函数**
- **命名规则:** 在Consumer类中生成一个函数封装了消息的消费逻辑的函数入口
- **命名规则:** 函数名为handleMessage
- **职责:** 消息的消费函数入口,该函数以对应的消息作为参数
- **唯一标识符位置:** 其对应的标识符在函数@AutoGenerated中指定, uuid规则: ${模块id}_${DomainMessage在TOCO中的uuid}
- **修改建议**
- handleMessage函数中实现业务逻辑 如果消费成功返回ture; 处理失败返回false 消息会被多次投递
- 如果消息要实现幂等消费则需要在handleMessage函数上添加@Transactional注解同时在Consumer类中覆盖父函数 @Override boolean useDBIdempotentWithTransactional(){return true;}, 开启幂等机制; 对于监听实体状态变化的事件,因为实体的任何字段变更都会触发事件,因此在消费消息的时候有些情况需要根据具体的字段值进行过滤, 例如:监听订单支付状态, 创建的消息叫做 order_changed_mo,包含了status字段那么仅当status_older!=payed && status=payed 为true的时候表示是订单支付完成的消息。
### **3 代码生成说明**
**3.1 技术栈**
Java、SpringBoot、MyBatis-plus(读取)、Hibernate(写入)
**3.2 项目结构**
- 多模块SpringBoot项目子模块在`/modules`目录下。子模块路径必须从`modules`开始,如:`modules/module1/src/main/java...`
- 根目录的entrance默认已经依赖了所有子模块的entrance模块无需额外引入pom配置。
```
项目根目录
|──main_module
│ ├── pom.xml # 项目级pom配置
│ ├── common # 项目级公共模块
│ │ ├──config/ # 中间件配置
│ │ ├──constants/ # 项目常量
│ │ ├──enums/ # 项目枚举
│ │ ├──redis/ # Redis配置
│ │ ├──response/ # 返回结果封装
│ │ └──utils/ # 项目工具类存放全项目复用的、或多模块复用的工具类如OSSUtils、UserTokenUtils等
└───├── entrance/ # 项目入口引用了项目中的所有模块通常适合放一些全局的Filter、Aspect等如AuthFilter、LogAspect等
│ ├──pom.xml # 项目级pom配置
│ └──AppApplication.java # 启动类
└── modules # 子模块列表
└── module1/ # 子模块1
├── pom.xml # 子模块1的pom配置
├── common/
│ ├──pom.xml # common的pom配置
│ ├──constants/ # 模块常量
│ ├──utils/ #模块工具类
│ └──enums/ # 枚举
├── entrance/web/src/main/java/com/{project_name}/{module_name}/entrance/web/
│ ├──pom.xml # entrance的pom配置
│ ├──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/ # DTO定义
│ ├── mo/ # 消息定义 (普通消息和领域消息)
│ ├── converter/ # 复杂Dto非BaseDto)组装
│ ├── facade/ # 调用其他模块的RPC适配器包含RpcAdapter如UserDtoServiceInMeetingRpcAdapter表示从meeting模块调用user模块中方法
│ └── impl/ # DTO查询实现
├── persist/src/main/java/com/{project_name}/{module_name}/persist/
│ ├── eo/ # 值对象定义
│ ├── dos/ # 数据库表映射
│ ├── qto/ # 读方案数据库查询
│ └── mapper/ # MyBatis Mapper
└── service/src/main/java/com/{project_name}/{module_name}/service/ # BOService包含某个聚合下所有写方案生成的方法、 DtoService包含DTO生成的预定义方法
├── mq/producer/ #普通消息的生成者
├── mq/consumer/ #消息的消费者
├── 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 特殊注解**
自动生成的类和方法有`@AutoGenerated`注解:
- `locked`属性true表示不建议修改
- `uuid`属性:唯一标识,包含`|`字符表示特殊格式拼装
**3.5 分层规则**
TOCO严格遵照分层规则必须严格遵守否则会产生严重后果
- VO以及相关的类属于entrance层
- Service禁止引用任何entrance层的类或代码
- Service层方法禁止返回VO禁止调用任何entrance层方法(如VoQueryExecutor、VoConverter等)
### 4. TOCO 最佳实践
#### 4.1 接口参数类型和返回值选择:
设计TOCO接口(API、RPC)时先判断主要功能是读库还是写库分析对应的读写方案及QTO、BTO。读场景**优先**使用QTO作参数写场景**优先**使用BTO作参数。QTO、BTO不满足时可增加基本类型、Enum、EO参数。必须遵循a.DTO、VO不能作参数b.QTO、BTO不能作返回值。
**重要说明:**
- 本节描述参数选择的**优先级原则**,非绝对限制
- API参数绝对限制见2.14章节
- "优先使用QTO/BTO"指在满足规范前提下,根据场景选择最合适的参数类型
#### 4.2 并发数据保护最佳实践:
处理先读后更新场景时为避免并发导致数据脏写应充分利用BoService校验功能。例如账户扣钱场景避免并发扣除导致余额不足传统raw sql用 `update account set balance = balance - amount where user_id = xxx and balance > amount` 的where条件保护在TOCO中应该1、写方案中使用incr字段。2、在boService中添加保护代码 `if(userBo.getBalance() >= 0)` 或通过userBo业务不变性聚合校验添加 `balance>=0` 校验。
-----------------------------------------------------------------------------
</TOCO知识库>