----------------------------------------------------------------------------- ### **1. TOCO 平台概览:** - **1.1 平台简介:** TOCO是软件设计和代码自动生成平台。基于DDD、分层架构、CQRS理论,覆盖数据库到API的完整开发链路,提升开发效率和代码质量 - **1.2 核心价值/目标用户:** 提供软件设计能力,设计成果直接生成标准代码,提高编码一致性和效率 - **1.3 主要特性概览:** 可视化设计、模型关联、多人协作、代码生成器 ### **2. TOCO 设计元素:** #### **2.1 模块 (Module)** - **定义与用途:** 在TOCO中,我们将系统领域细分为具体的模块,映射为Java工程中的module。这些模块代表了系统的叶子子域,每个模块负责特定的功能。模块划分有助于系统的可维护性和可扩展性,并能提高开发效率和代码质量 - **关键配置:** 名称(小写英文+下划线,如meeting,user_detail,禁止后缀,全局唯一),描述 - **与其他元素关系:** 下列所有设计元素都属于某个模块 #### **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"] } ``` #### **2.3 值对象 (EO)** - **定义与用途:** EO是可复用的POJO数据结构,可跨模块使用,可作为实体字段类型。 - **关键属性/配置:** 名称以_eo结尾,全局唯一。字段类型限制:基本类型、List、EO、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"] } ``` #### **2.4 实体关系 (ER / Entity)** - **定义与用途:** 实体对应数据库表,关系为实体间的外键依赖 - **关键属性/配置:** 包含名称、字段、字段类型、主键、索引,关系分为1:1和1:N - **与其他元素关系:** 实体是聚合的基础,也是DTO和VO的派生基础 #### **2.5 聚合对象 (BO/业务对象)** - **定义与用途:** 聚合对象封装一组关联实体。从聚合根实体开始,按层级关系组装其他实体,形成树形结构。提供内存一致性视图和数据操作入口。一个实体只能属于一个聚合对象,聚合对象只能在单一模块中组合。 - **包含元素:** 聚合根实体 + 子实体对象。例如:ProductBO包含商品基本信息实体(聚合根)、商品SKU实体、商品库存实体(子对象)。 - **关键配置:** 名称(${EntityName驼峰}BO,如StaffBO),聚合根实体,子对象实体。每个聚合必须包含一个聚合根。 - **与其他元素关系:** 聚合是写方案的基础。 #### **2.6 数据传输对象 (DTO)** - **定义与用途:** DTO基于某个Entity构建,通过外键关系关联多个Entity的数据结构。DTO隐含数据组装逻辑,符合外键关系。分为BaseDTO和普通DTO:BaseDTO直接派生自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;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反向注入到MeetingRoom中: ``` MeetingRoomWithMeetingDto { Long id;// 会议室id String name;// 会议室名称 List 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为类结构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_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 UserDtoService.getByUserNames(List userNames)。预定义方法内部基于外键关系自动生成复杂DTO数据的递归、Join拼装能力,直接返回DTO内部所有继承字段和扩展字段数据。 - **公开性:** 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种方式 判断使用方式时,只能根据查询条件判断是否使用读方案,禁止使用返回值是否需要数据拼装来判断! #### **2.7 视图对象 (VO)** - **定义与用途:** VO基于BaseDTO(或指定的其他DTO)派生,通过外键关系关联多个BaseDTO的数据结构。用于视图层与前端数据传输,作为HTTP API返回值或读方案返回值。不能作为接口参数或RPC返回值。 - **关键配置:** 名称(以Vo结尾,全局唯一)、根Entity、派生源、字段列表。字段分三种:a.继承DTO字段(基础类型/EO/Enum保持类型不变,DTO类型转为对应VO类型);b.扩展字段(正向替换和反向注入,类型为VO或List);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通常基于某个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 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为类结构UUID(type为List且innerType=Enum、Eo时包含);extendFieldList中,name为继承字段名,若DTO字段非DTO类型则VO字段类型与DTO一致,若DTO字段为DTO或List类型,由于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", "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,即这两个方法已**自动**获取所有**继承字段**和**扩展字段**数据。 - **字段数据获取:** 继承自DTO的字段及扩展字段,TOCO在convert方法中会自动组装数据,无需手动实现获取和拼装逻辑。自定义字段则需要在coding阶段实现对应获取和拼装逻辑,便于其他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种 注意判断使用方式时,只能根据查询条件判断是否使用读方案,绝对禁止使用返回值是否需要数据拼装来判断! #### **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 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通过外键关系替换/注入对应Entity信息,对象化表达有外键关系的Entity信息。存在外键关系且满足条件即可扩展:a.正向替换:当前实体有指向其他实体的外键字段;b.反向注入:其他实体有指向当前实体的外键字段。 例如:两个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反向注入到MeetingRoom中: ``` MeetingRoomWithMeetingWo { Long id;// 会议室id String name;// 会议室名称 List 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外键关系只能使用exists,1:1外键关系可使用left join和exists 例如:实体t和t1,t1有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 bDto,BDto有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 定义了如下语法 @top Program { expression? } @skip { spaces | newline | LineComment } @precedence { member, and @left, or @left } kw { @specialize[@name={term}] } boolean { @specialize[@name=Boolean] } @skip {} { String[isolate] { '"' (stringContentDouble | Escape)* ('"' | "\n") | "'" (stringContentSingle | Escape)* ("'" | "\n") } } commaSep { content ("," content)* } List { "[" commaSep ~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"] } } ``` - **举例** - 上下文 public class meeting_room { storey storey; // 楼层对象 String name; // 名称 room_type_enum room_type; // 会议室类型 List 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; // 会议列表,可添加过滤条件 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 } } - 用户需求 获取一段时间未使用的会议室(包含当前会议已选择的会议室),根据会议标题过滤会议信息,分页返回。 - 读方案定义 { "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" } ] } #### **2.10 查询传输对象(QTO)** - **定义与用途:** QTO是读方案的查询参数结构。每个读方案对应一个QTO。调用方使用QTO结构传入查询字段值,完成数据库查询 - **如何创建/生成:** 创建读方案后,TOCO自动生成对应的QTO。无需单独创建 - **关键配置:** 名称(${ReadPlanNameQto},驼峰命名),查询字段列表(如idIs,nameLike,schoolNameLike等) - **与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 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 + ?,传入负值可减值 - **写方案设计元素表达:** ```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":"增量操作字段列表" }, "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 meetingAgendaList; class meeting_agenda { Long id;//主键 Date start_time;//开始时间 Date end_time;//结束时间 String title;//议程名称 List 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" ] } ] } ``` #### **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 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的读方案根据分页和计数配置自动生成非公开RPC;c.写方案创建后自动生成非公开RPC,返回聚合根实体主键值;d.需求不满足时通过TOCO创建自定义RPC,需指定参数、返回值、公开性 - **优先复用:** 用户需要创建RPC时,有明确要求按要求创建。无明确要求时先判断能否通过读方案、写方案、DTO自动创建RPC,最后考虑自定义RPC - **自定义RPC和手写方法关系:** 两者都可手动实现服务层方法。区别:需要被其他模块订阅用自定义RPC;只是API私有调用不需要外部开放用手写方法 - **关键配置:** 类名(驼峰,首字母大写,以Service结尾)、是否公开、方法名(驼峰,首字母小写)、请求参数、返回值。分页查询参数为Qto类型时,Qto已包含from、size、scrollId属性,无需额外参数 - **TOCO中RPC存储:** RPC在TOCO中只存储方法签名,不存储执行逻辑。 - **参数类型:** 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为返回结构,参数和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;description为描述;typeUuid为类结构UUID,type为Enum、Eo、Dto时传入对象uuid,Qto时传入读方案uuid,Bto时传入写方案uuid;innerType为List内部类型,type为List或PageResult时包含;innerUuid为List内部类结构UUID,type为List或PageResult且innerType为Enum、Eo、Dto时传入对象uuid,innerType为Qto时传入读方案uuid,Bto时传入写方案uuid #### **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、基本类型,可为单值或列表。禁用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);description为描述;typeUuid为类结构UUID(type为Enum、Eo、Vo时传入对象uuid,Qto时传入读方案uuid,Bto时传入写方案uuid);innerType为List内部类型(type为List时包含);innerUuid为List或PageResult内部类结构UUID(type为List或PageResult且innerType为Enum、Eo、Vo时传入对象uuid,innerType为Qto时传入读方案uuid,Bto时传入写方案uuid) #### **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" } ] } ``` #### **2.16 自定义查询** - 读方案无法满足需求时,使用自定义查询 - 自定义查询使用复杂sql实现业务功能 - 数据访问层使用MyBatis、MyBatisPlus实现 - 自定义查询时框架不自动生成代码(需要在coding阶段手动编写) ### 3. TOCO 最佳实践 #### 3.1 接口参数类型和返回值选择: 设计TOCO接口(API、RPC)时,先判断主要功能是读库还是写库,分析对应的读写方案及QTO、BTO。读场景**优先**使用QTO作参数;写场景**优先**使用BTO作参数。QTO、BTO不满足时,可增加基本类型、Enum、EO参数。必须遵循:a.DTO、VO不能作参数;b.QTO、BTO不能作返回值。 **重要说明:** - 本节描述参数选择的**优先级原则**,非绝对限制 - API参数绝对限制见2.14章节 - "优先使用QTO/BTO"指在满足规范前提下,根据场景选择最合适的参数类型 #### 3.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` 校验。 -----------------------------------------------------------------------------