package com.vincent.rsf.server.common; import com.vincent.rsf.framework.generators.ReactGenerator; import com.vincent.rsf.framework.generators.RsfDesignGenerator; import com.vincent.rsf.framework.generators.constant.SqlOsType; import com.vincent.rsf.framework.generators.domain.Column; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class RsfDesignGeneratorRegressionTest { @TempDir Path tempDir; @Test void buildFrontendArtifactsKeepsDisplayColumnsCommaAndNestedApiPath() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_test", "系统测试表", "test", "Test", "system/test", "system/test", buildColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); Path tableColumnsFile = tempDir.resolve("src/views/system/test/testTable.columns.js"); Path pageHelpersFile = tempDir.resolve("src/views/system/test/testPage.helpers.js"); Path apiFile = tempDir.resolve("src/api/system/test.js"); String tableColumnsContent = Files.readString(tableColumnsFile, StandardCharsets.UTF_8); String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(Files.exists(apiFile), "嵌套 api 模块路径应生成到 src/api/system/test.js"), () -> assertTrue(tableColumnsContent.contains("createTextColumn('levelText', '等级', 160),"), "数值枚举列应显示为 levelText"), () -> assertTrue(tableColumnsContent.contains("createTextColumn('roleIdText', '角色', 160),"), "数值外键列应显示为 roleIdText"), () -> assertFalse(tableColumnsContent.contains("createNumberColumn('level',"), "数值枚举列不应再走原始数字列"), () -> assertFalse(tableColumnsContent.contains("createNumberColumn('roleId',"), "数值外键列不应再走原始数字列"), () -> assertTrue( tableColumnsContent.contains("createTextColumn('memo', '备注', 220),\r\n {") || tableColumnsContent.contains("createTextColumn('memo', '备注', 220),\n {"), "最后一个业务列和操作列之间应保留逗号" ), () -> assertTrue(pageHelpersContent.contains("{ source: 'levelText', label: '等级' }"), "报表列应包含 levelText"), () -> assertTrue(pageHelpersContent.contains("{ source: 'roleIdText', label: '角色' }"), "报表列应包含 roleIdText") ); } @Test void buildFrontendArtifactsKeepsBooleanAndStatusTextMappingForPrintExport() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_switch", "系统开关表", "switchItem", "SwitchItem", "system/switch-item", "system/switch-item", buildBooleanColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); Path tableColumnsFile = tempDir.resolve("src/views/system/switch-item/switchItemTable.columns.js"); Path pageHelpersFile = tempDir.resolve("src/views/system/switch-item/switchItemPage.helpers.js"); String tableColumnsContent = Files.readString(tableColumnsFile, StandardCharsets.UTF_8); String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(tableColumnsContent.contains("createTextColumn('enabledText', '是否启用', 160),"), "布尔字段列表列应显示为 enabledText"), () -> assertTrue(tableColumnsContent.contains("createTagColumn('status', '状态', 120"), "status 字段应继续走标签列"), () -> assertTrue(pageHelpersContent.contains("enabledText: formatBooleanText(toOptionalBoolean(record.enabled))"), "布尔字段应生成 formatBooleanText 映射"), () -> assertTrue(pageHelpersContent.contains("{ source: 'enabledText', label: '是否启用' }"), "布尔字段报表列应使用 enabledText"), () -> assertTrue(pageHelpersContent.contains("{ source: 'statusText', label: '状态' }"), "status 报表列应使用 statusText"), () -> assertTrue(pageHelpersContent.contains("status: 'statusText'"), "status 导出列别名应映射到 statusText") ); } @Test void buildFrontendArtifactsSupportsDeepNestedViewAndApiModules() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_deep_case", "深层目录测试表", "deepCase", "DeepCase", "basic-info/warehouse/deep-case", "system/admin/deep-case", buildColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); assertAll( () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/index.vue")), "深层 view 路径应生成 index.vue"), () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/deepCasePage.helpers.js")), "深层 view 路径应生成 helpers"), () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/deepCaseTable.columns.js")), "深层 view 路径应生成列文件"), () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/modules/deep-case-search.vue")), "深层 view 路径应生成 search 组件"), () -> assertTrue(Files.exists(tempDir.resolve("src/views/basic-info/warehouse/deep-case/modules/deep-case-edit-dialog.vue")), "深层 view 路径应生成 edit dialog"), () -> assertTrue(Files.exists(tempDir.resolve("src/api/system/admin/deep-case.js")), "深层 api 模块路径应生成到正确目录") ); } @Test void buildFrontendArtifactsKeepsFormDefaultsPayloadAndRulesAligned() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_default_case", "默认值测试表", "defaultCase", "DefaultCase", "system/default-case", "system/default-case", buildDefaultCaseColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); Path pageHelpersFile = tempDir.resolve("src/views/system/default-case/defaultCasePage.helpers.js"); Path editDialogFile = tempDir.resolve("src/views/system/default-case/modules/default-case-edit-dialog.vue"); String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); String editDialogContent = Files.readString(editDialogFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(pageHelpersContent.contains("status: 1,"), "状态字段表单默认值应使用首个枚举值"), () -> assertTrue(pageHelpersContent.contains("level: void 0,"), "数字字段表单默认值应为 void 0"), () -> assertTrue(pageHelpersContent.contains("enabled: void 0,"), "布尔字段表单默认值应为 void 0"), () -> assertTrue(pageHelpersContent.contains("status: hasValue(record.status) ? toOptionalNumber(record.status) : 1,"), "状态字段弹窗模型应保留默认回退值"), () -> assertTrue(pageHelpersContent.contains("...buildNumberField('status', formData.status),"), "状态字段保存 payload 应走数字字段构造"), () -> assertTrue(pageHelpersContent.contains("...(hasValue(formData.enabled) ? { enabled: toOptionalBoolean(formData.enabled) } : {}),"), "布尔字段保存 payload 应走布尔转换"), () -> assertTrue(editDialogContent.contains("createInputFormItem('名称', 'name', '请输入名称'),"), "普通文本字段应生成输入框"), () -> assertTrue(editDialogContent.contains("createInputFormItem('等级', 'level', '请输入等级', { type: 'number' }),"), "数字字段应生成 number 输入框"), () -> assertTrue(editDialogContent.contains("createSelectFormItem('是否启用', 'enabled', '请选择是否启用', getDefaultCaseFieldOptions('enabled')),") , "布尔字段应生成下拉项"), () -> assertTrue(editDialogContent.contains("createSelectFormItem('状态', 'status', '请选择状态', getDefaultCaseFieldOptions('status')),") , "枚举状态字段应生成下拉项"), () -> assertTrue(editDialogContent.contains("name: [{ required: true, message: '请输入名称', trigger: 'blur' }],"), "文本必填规则应使用 blur/请输入"), () -> assertTrue(editDialogContent.contains("status: [{ required: true, message: '请选择状态', trigger: 'change' }]"), "选择类必填规则应使用 change/请选择") ); } @Test void buildFrontendArtifactsKeepsSearchItemsAndQueryParamsAligned() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_search_case", "搜索测试表", "searchCase", "SearchCase", "system/search-case", "system/search-case", buildSearchCaseColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); Path pageHelpersFile = tempDir.resolve("src/views/system/search-case/searchCasePage.helpers.js"); Path searchFile = tempDir.resolve("src/views/system/search-case/modules/search-case-search.vue"); String pageHelpersContent = Files.readString(pageHelpersFile, StandardCharsets.UTF_8); String searchContent = Files.readString(searchFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(pageHelpersContent.contains("keyword: '',"), "文本搜索字段应出现在搜索状态中"), () -> assertTrue(pageHelpersContent.contains("level: '',"), "数字搜索字段应出现在搜索状态中"), () -> assertTrue(pageHelpersContent.contains("enabled: '',"), "布尔搜索字段应出现在搜索状态中"), () -> assertTrue(pageHelpersContent.contains("status: ''"), "枚举搜索字段应出现在搜索状态中"), () -> assertFalse(pageHelpersContent.contains("createTime: '',"), "日期字段不应进入搜索状态"), () -> assertFalse(pageHelpersContent.contains("deleted: '',"), "托管字段不应进入搜索状态"), () -> assertTrue(pageHelpersContent.contains("keyword: normalizeText(params.keyword),"), "文本搜索参数应走 normalizeText"), () -> assertTrue(pageHelpersContent.contains("level: toOptionalNumber(params.level),"), "数字搜索参数应走 toOptionalNumber"), () -> assertTrue(pageHelpersContent.contains("enabled: toOptionalBoolean(params.enabled),"), "布尔搜索参数应走 toOptionalBoolean"), () -> assertTrue(pageHelpersContent.contains("status: toOptionalNumber(params.status)"), "枚举数字搜索参数应走 toOptionalNumber"), () -> assertTrue(pageHelpersContent.contains("current: params.current || 1,"), "分页参数应统一 current 默认值"), () -> assertTrue(pageHelpersContent.contains("pageSize: params.pageSize || params.size || 20,"), "分页参数应兼容 size/pageSize"), () -> assertTrue(searchContent.contains("createInputSearchItem('关键字', 'condition', '请输入搜索测试表关键字'),"), "搜索栏应始终包含 condition 关键字搜索"), () -> assertTrue(searchContent.contains("createInputSearchItem('关键字字段', 'keyword', '请输入关键字字段'),"), "文本搜索字段应生成输入框"), () -> assertTrue(searchContent.contains("createInputSearchItem('等级', 'level', '请输入等级'),"), "数字搜索字段当前应生成输入框"), () -> assertTrue(searchContent.contains("createSelectSearchItem('是否启用', 'enabled', '请选择是否启用', getSearchCaseFieldOptions('enabled')),") , "布尔搜索字段应生成下拉项"), () -> assertTrue(searchContent.contains("createSelectSearchItem('状态', 'status', '请选择状态', getSearchCaseFieldOptions('status'))") , "枚举搜索字段应生成下拉项"), () -> assertFalse(searchContent.contains("createTime"), "日期字段不应生成搜索项"), () -> assertFalse(searchContent.contains("deleted"), "托管字段不应生成搜索项") ); } @Test void writeControllerTemplateKeepsCrudAndExportContractAligned() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_controller_case", "控制器测试表", "controllerCase", "ControllerCase", "system/controller-case", "system/controller-case", buildControllerCaseColumns() ); generator.backendPrefixPath = tempDir.toString().replace("\\", "/"); String controllerDirectory = (String) invokePrivate(generator, "resolveControllerDirectory"); invokePrivate( generator, "writeTemplate", new Class[]{String.class, String.class, String.class}, new Object[]{"Controller", controllerDirectory, "ControllerCaseController.java"} ); Path controllerFile = tempDir.resolve("src/main/java/com/vincent/rsf/server/system/controller/ControllerCaseController.java"); String controllerContent = Files.readString(controllerFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(controllerContent.contains("public class ControllerCaseController extends BaseController"), "控制器类名应和实体保持一致"), () -> assertTrue(controllerContent.contains("private ControllerCaseService controllerCaseService;"), "控制器应注入 service"), () -> assertTrue(controllerContent.contains("private ListExportService listExportService;"), "控制器应注入导出服务"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/page\")"), "分页接口路径应保持 page"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/save\")"), "新增接口路径应保持 save"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/update\")"), "更新接口路径应保持 update"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/remove/{ids}\")"), "删除接口路径应保持 remove"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/query\")"), "查询接口路径应保持 query"), () -> assertTrue(controllerContent.contains("@PostMapping(\"/controllerCase/export\")"), "导出接口路径应保持 export"), () -> assertTrue(controllerContent.contains("listExportService.export(map, exportMap -> buildParam(exportMap, BaseParam.class), controllerCaseExportHandler, response);"), "导出接口应继续走 ListExportService"), () -> assertTrue(controllerContent.contains("controllerCase.setCreateBy(getLoginUserId());"), "新增初始化应补 createBy"), () -> assertTrue(controllerContent.contains("controllerCase.setCreateTime(new Date());"), "新增初始化应补 createTime"), () -> assertTrue(controllerContent.contains("controllerCase.setUpdateBy(getLoginUserId());"), "新增和更新初始化都应补 updateBy"), () -> assertTrue(controllerContent.contains("controllerCase.setUpdateTime(new Date());"), "新增和更新初始化都应补 updateTime"), () -> assertTrue(controllerContent.contains("row.put(\"statusText\", record.getStatus$());"), "导出行应输出 statusText"), () -> assertTrue(controllerContent.contains("row.put(\"ownerIdText\", record.getOwnerId$());"), "导出行应输出外键文本字段"), () -> assertTrue(controllerContent.contains("wrapper.like(ControllerCase::getName, condition);"), "query 接口应按主字段模糊查询"), () -> assertTrue(controllerContent.contains("new KeyValVo(item.getId(), item.getName())"), "query 接口应返回主键和主字段") ); } @Test void buildFrontendArtifactsKeepsApiRequestContractAligned() throws Exception { RsfDesignGenerator generator = createGenerator( "sys_api_case", "接口测试表", "apiCase", "ApiCase", "system/api-case", "system/api-case", buildColumns() ); invokePrivate(generator, "buildFrontendArtifacts"); Path apiFile = tempDir.resolve("src/api/system/api-case.js"); String apiContent = Files.readString(apiFile, StandardCharsets.UTF_8); assertAll( () -> assertTrue(apiContent.contains("function normalizeIds(ids) {"), "api 文件应包含 ids 归一化工具"), () -> assertTrue(apiContent.contains("export function fetchApiCasePage(params = {}) {"), "应生成 page 请求函数"), () -> assertTrue(apiContent.contains("url: '/apiCase/page'"), "page 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchApiCaseList(params = {}) {"), "应生成 list 请求函数"), () -> assertTrue(apiContent.contains("url: '/apiCase/list'"), "list 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchGetApiCaseDetail(id) {"), "应生成 detail 请求函数"), () -> assertTrue(apiContent.contains("url: `/apiCase/${id}`"), "detail 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchGetApiCaseMany(ids) {"), "应生成 many 请求函数"), () -> assertTrue(apiContent.contains("url: `/apiCase/many/${normalizeIds(ids)}`"), "many 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchSaveApiCase(params = {}) {"), "应生成 save 请求函数"), () -> assertTrue(apiContent.contains("url: '/apiCase/save'"), "save 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchUpdateApiCase(params = {}) {"), "应生成 update 请求函数"), () -> assertTrue(apiContent.contains("url: '/apiCase/update'"), "update 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchDeleteApiCase(ids) {"), "应生成 delete 请求函数"), () -> assertTrue(apiContent.contains("url: `/apiCase/remove/${normalizeIds(ids)}`"), "delete 请求路径应正确"), () -> assertTrue(apiContent.contains("export function fetchApiCaseQuery(condition = '') {"), "应生成 query 请求函数"), () -> assertTrue(apiContent.contains("url: '/apiCase/query'"), "query 请求路径应正确"), () -> assertTrue(apiContent.contains("condition: normalizeText(condition)"), "query 请求应对 condition 做 trim"), () -> assertTrue(apiContent.contains("export async function fetchExportApiCaseReport(payload = {}, options = {}) {"), "应生成 export 请求函数"), () -> assertTrue(apiContent.contains("fetch(`${import.meta.env.VITE_API_URL}/apiCase/export`"), "export 应直接请求完整导出地址"), () -> assertTrue(apiContent.contains("method: 'POST'"), "export 请求应使用 POST"), () -> assertTrue(apiContent.contains("body: JSON.stringify(payload)"), "export 请求应发送 JSON body") ); } @Test void writeBackendArtifactsKeepEntityServiceMapperXmlAndSqlAligned() throws Exception { ReactGenerator generator = createReactGenerator( "sys_backend_case", "后端测试表", "backendCase", "BackendCase", buildBackendCaseColumns() ); writeReactTemplate(generator, "Entity", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/entity").toString() + "/", "BackendCase.java"); writeReactTemplate(generator, "Service", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service").toString() + "/", "BackendCaseService.java"); writeReactTemplate(generator, "ServiceImpl", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/impl").toString() + "/", "BackendCaseServiceImpl.java"); writeReactTemplate(generator, "Mapper", tempDir.resolve("src/main/java/com/vincent/rsf/server/system/mapper").toString() + "/", "BackendCaseMapper.java"); writeReactTemplate(generator, "Xml", tempDir.resolve("src/main/resources/mapper/system").toString() + "/", "BackendCaseMapper.xml"); writeReactTemplate(generator, "Sql", tempDir.resolve("src/main/java").toString() + "/", "backendCase.sql"); Charset systemCharset = Charset.defaultCharset(); String entityContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/entity/BackendCase.java"), systemCharset); String serviceContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/BackendCaseService.java"), systemCharset); String serviceImplContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/service/impl/BackendCaseServiceImpl.java"), systemCharset); String mapperContent = Files.readString(tempDir.resolve("src/main/java/com/vincent/rsf/server/system/mapper/BackendCaseMapper.java"), systemCharset); String xmlContent = Files.readString(tempDir.resolve("src/main/resources/mapper/system/BackendCaseMapper.xml"), systemCharset); String sqlContent = Files.readString(tempDir.resolve("src/main/java/backendCase.sql"), systemCharset); assertAll( () -> assertTrue(entityContent.contains("@TableName(\"sys_backend_case\")"), "实体应绑定真实表名"), () -> assertTrue(entityContent.contains("private Long id;"), "实体应生成主键字段"), () -> assertTrue(entityContent.contains("private String name;"), "实体应生成普通文本字段"), () -> assertTrue(entityContent.contains("private Long ownerId;"), "实体应生成外键字段"), () -> assertTrue(entityContent.contains("private Integer status;"), "实体应生成状态字段"), () -> assertTrue(entityContent.contains("@TableLogic"), "实体应为 deleted 字段添加逻辑删除注解"), () -> assertTrue(entityContent.contains("@DateTimeFormat(pattern=\"yyyy-MM-dd HH:mm:ss\")"), "实体应为日期字段添加时间格式注解"), () -> assertTrue(entityContent.contains("public Boolean getStatusBool(){"), "实体应保留 statusBool 便捷方法"), () -> assertTrue(serviceContent.contains("public interface BackendCaseService extends IService"), "Service 接口应继承 IService"), () -> assertTrue(serviceImplContent.contains("public class BackendCaseServiceImpl extends ServiceImpl implements BackendCaseService"), "ServiceImpl 应绑定 mapper 和 entity"), () -> assertTrue(serviceImplContent.contains("@Service(\"backendCaseService\")"), "ServiceImpl bean 名应使用小驼峰"), () -> assertTrue(mapperContent.contains("public interface BackendCaseMapper extends BaseMapper"), "Mapper 应继承 BaseMapper"), () -> assertTrue(xmlContent.contains(""), "Xml namespace 应指向正确 mapper"), () -> assertTrue(sqlContent.contains("insert into `sys_menu`"), "Sql 模板应生成菜单 SQL"), () -> assertTrue(sqlContent.contains("'system:backendCase:list'"), "Sql 模板应生成 list 权限"), () -> assertTrue(sqlContent.contains("'system:backendCase:save'"), "Sql 模板应生成 save 权限"), () -> assertTrue(sqlContent.contains("'system:backendCase:update'"), "Sql 模板应生成 update 权限"), () -> assertTrue(sqlContent.contains("'system:backendCase:remove'"), "Sql 模板应生成 remove 权限"), () -> assertTrue(sqlContent.contains("backendCase: 'BackendCase'"), "Sql 模板应生成 locale 菜单名"), () -> assertTrue(sqlContent.contains("import backendCase from './backendCase';"), "Sql 模板应生成资源导入片段") ); } private List buildColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); Column level = new Column(null, "level", "Integer", "等级{1:高,0:低}", false, false, false, 11, false, SqlOsType.MYSQL); Column roleId = new Column(null, "role_id", "Long", "角色", false, false, false, 20, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); Column createTime = new Column(null, "create_time", "Date", "添加时间", false, false, false, null, false, SqlOsType.MYSQL); Column updateTime = new Column(null, "update_time", "Date", "修改时间", false, false, false, null, false, SqlOsType.MYSQL); Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); roleId.setForeignKeyMajor("Name"); return List.of(id, name, level, roleId, status, deleted, createTime, updateTime, memo); } private List buildBooleanColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); return List.of(id, name, enabled, status, deleted, memo); } private List buildDefaultCaseColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); Column level = new Column(null, "level", "Integer", "等级", false, false, false, 11, false, SqlOsType.MYSQL); Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); Column memo = new Column(null, "memo", "String", "备注", false, false, false, 255, false, SqlOsType.MYSQL); return List.of(id, name, level, enabled, status, deleted, memo); } private List buildSearchCaseColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column keyword = new Column(null, "keyword", "String", "关键字字段", false, false, false, 255, false, SqlOsType.MYSQL); Column level = new Column(null, "level", "Integer", "等级", false, false, false, 11, false, SqlOsType.MYSQL); Column enabled = new Column(null, "enabled", "Boolean", "是否启用", false, false, false, 1, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, false, 1, false, SqlOsType.MYSQL); Column createTime = new Column(null, "create_time", "Date", "添加时间", false, false, false, null, false, SqlOsType.MYSQL); Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); return List.of(id, keyword, level, enabled, status, createTime, deleted); } private List buildControllerCaseColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); Column ownerId = new Column(null, "owner_id", "Long", "负责人", false, false, false, 20, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); Column createBy = new Column(null, "create_by", "Long", "创建人", false, false, false, 20, false, SqlOsType.MYSQL); Column createTime = new Column(null, "create_time", "Date", "创建时间", false, false, false, null, false, SqlOsType.MYSQL); Column updateBy = new Column(null, "update_by", "Long", "更新人", false, false, false, 20, false, SqlOsType.MYSQL); Column updateTime = new Column(null, "update_time", "Date", "更新时间", false, false, false, null, false, SqlOsType.MYSQL); ownerId.setForeignKeyMajor("Name"); return List.of(id, name, ownerId, status, createBy, createTime, updateBy, updateTime); } private List buildBackendCaseColumns() { Column id = new Column(null, "id", "Long", "ID", true, false, true, 20, false, SqlOsType.MYSQL); Column name = new Column(null, "name", "String", "名称(*)", false, false, true, 255, false, SqlOsType.MYSQL); Column ownerId = new Column(null, "owner_id", "Long", "负责人", false, false, false, 20, false, SqlOsType.MYSQL); Column status = new Column(null, "status", "Integer", "状态{1:正常,0:禁用}", false, false, true, 1, false, SqlOsType.MYSQL); Column deleted = new Column(null, "deleted", "Integer", "是否删除{1:是,0:否}", false, false, true, 1, false, SqlOsType.MYSQL); Column createTime = new Column(null, "create_time", "Date", "创建时间", false, false, false, null, false, SqlOsType.MYSQL); ownerId.setForeignKey("User"); ownerId.setForeignKeyMajor("Name"); return List.of(id, name, ownerId, status, deleted, createTime); } private RsfDesignGenerator createGenerator( String table, String tableDesc, String simpleEntityName, String fullEntityName, String frontendViewPath, String frontendApiModule, List columns ) throws Exception { RsfDesignGenerator generator = new RsfDesignGenerator(); generator.table = table; generator.tableDesc = tableDesc; generator.packagePath = "com.vincent.rsf.server.system"; generator.frontendPrefixPath = tempDir.toString().replace("\\", "/"); generator.frontendViewPath = frontendViewPath; generator.frontendApiModule = frontendApiModule; generator.sqlOsType = SqlOsType.MYSQL; setField(generator, "columns", columns); setField(generator, "fullEntityName", fullEntityName); setField(generator, "simpleEntityName", simpleEntityName); setField(generator, "kebabEntityName", toKebab(simpleEntityName)); setField(generator, "constantPrefix", simpleEntityName.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toUpperCase()); setField(generator, "primaryKeyColumn", "id"); setField(generator, "majorColumn", "name"); setField(generator, "itemName", "system"); setField(generator, "normalizedFrontendViewPath", frontendViewPath); setField(generator, "normalizedFrontendApiModule", frontendApiModule); return generator; } private String toKebab(String value) { return value.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(); } private ReactGenerator createReactGenerator( String table, String tableDesc, String simpleEntityName, String fullEntityName, List columns ) throws Exception { ReactGenerator generator = new ReactGenerator(); generator.table = table; generator.tableDesc = tableDesc; generator.packagePath = "com.vincent.rsf.server.system"; generator.backendPrefixPath = tempDir.toString().replace("\\", "/") + "/"; generator.frontendPrefixPath = tempDir.toString().replace("\\", "/"); setField(generator, "columns", columns); setField(generator, "fullEntityName", fullEntityName); setField(generator, "simpleEntityName", simpleEntityName); setField(generator, "itemName", "system"); setField(generator, "systemPackagePath", "com.vincent.rsf.server.system"); setField(generator, "systemPackage", "com.vincent.rsf.server.system"); setField(generator, "entityContent", invokePrivate(generator, "createEntityMsg")); setField(generator, "primaryKeyColumn", invokePrivate(generator, "createPrimaryMsg")); setField(generator, "majorColumn", invokePrivate(generator, "createMajorMsg")); setField(generator, "reactLocaleContent", invokePrivate(generator, "createReactLocaleContent")); return generator; } private void writeReactTemplate(ReactGenerator generator, String templateName, String directory, String fileName) throws Exception { String content = (String) invokePrivate(generator, "readFile", new Class[]{String.class}, new Object[]{templateName}); invokePrivate( generator, "writeFile", new Class[]{String.class, String.class, String.class, String.class}, new Object[]{content, directory.replace("\\", "/"), fileName, templateName} ); } private void setField(Object target, String name, Object value) throws Exception { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); field.set(target, value); } private Object invokePrivate(Object target, String name) throws Exception { Method method = target.getClass().getDeclaredMethod(name); method.setAccessible(true); return method.invoke(target); } private Object invokePrivate(Object target, String name, Class[] parameterTypes, Object[] args) throws Exception { Method method = target.getClass().getDeclaredMethod(name, parameterTypes); method.setAccessible(true); return method.invoke(target, args); } }