diff --git a/app/src/main/java/com/tinyengine/it/TinyEngineApplication.java b/app/src/main/java/com/tinyengine/it/TinyEngineApplication.java index 9535031f..78c56cd8 100644 --- a/app/src/main/java/com/tinyengine/it/TinyEngineApplication.java +++ b/app/src/main/java/com/tinyengine/it/TinyEngineApplication.java @@ -24,7 +24,7 @@ */ @SpringBootApplication @EnableAspectJAutoProxy -@MapperScan("com.tinyengine.it.mapper") +@MapperScan({"com.tinyengine.it.mapper","com.tinyengine.it.dynamic.dao"}) public class TinyEngineApplication { /** * The entry point of application. diff --git a/base/src/main/java/com/tinyengine/it/dynamic/controller/ModelDataController.java b/base/src/main/java/com/tinyengine/it/dynamic/controller/ModelDataController.java new file mode 100644 index 00000000..64195144 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/controller/ModelDataController.java @@ -0,0 +1,117 @@ +package com.tinyengine.it.dynamic.controller; + +import com.tinyengine.it.common.base.Result; +import com.tinyengine.it.common.log.SystemControllerLog; +import com.tinyengine.it.dynamic.dto.DynamicDelete; +import com.tinyengine.it.dynamic.dto.DynamicInsert; +import com.tinyengine.it.dynamic.dto.DynamicQuery; +import com.tinyengine.it.dynamic.dto.DynamicUpdate; +import com.tinyengine.it.dynamic.service.DynamicService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Validated +@Slf4j +@RestController +@RequestMapping("/platform-center/api") +@Tag(name = "模型数据") +public class ModelDataController { + @Autowired + private DynamicService dynamicService; + + /** + * 模型数据查询 + * + * @return 返回值 all + */ + @Operation(summary = "模型数据查询", description = "模型数据查询", responses = { + @ApiResponse(responseCode = "200", description = "返回信息", + content = @Content(mediaType = "application/json",schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "请求失败") + }) + @SystemControllerLog(description = "模型数据查询") + @PostMapping("/model-data/queryApi") + public Result> query(@RequestBody @Valid DynamicQuery dto) { + try { + return Result.success(dynamicService.queryWithPage(dto)); + } catch (Exception e) { + log.error("Query failed for table: {}", dto.getNameEn(), e); + return Result.failed("Query operation failed"); + } + + } + + /** + * 新增模型数据 + * + * @return 返回值 map + */ + @Operation(summary = "新增模型数据", description = "新增模型数据", responses = { + @ApiResponse(responseCode = "200", description = "返回信息", + content = @Content(mediaType = "application/json",schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "请求失败") + }) + @SystemControllerLog(description = "新增模型数据") + @PostMapping("/model-data/insertApi") + public Result > insert(@RequestBody @Valid DynamicInsert dto) { + try { + return Result.success(dynamicService.insert(dto)); + } catch (Exception e) { + log.error("insert failed for table: {}", dto.getNameEn(), e); + return Result.failed("insert operation failed"); + } + + } + + /** + * 更新模型数据 + * + * @return 返回值 map + */ + @Operation(summary = "更新模型数据", description = "更新模型数据", responses = { + @ApiResponse(responseCode = "200", description = "返回信息", + content = @Content(mediaType = "application/json",schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "请求失败") + }) + @SystemControllerLog(description = "更新模型数据") + @PostMapping("/model-data/updateApi") + public Result > update(@RequestBody @Valid DynamicUpdate dto) { + try { + return Result.success(dynamicService.update(dto)); + } catch (Exception e) { + log.error("updateApi failed for table: {}", dto.getNameEn(), e); + return Result.failed("update operation failed"); + } + + } + /** + * 刪除模型数据 + * + * @return 返回值 map + */ + @Operation(summary = "刪除模型数据", description = "刪除模型数据", responses = { + @ApiResponse(responseCode = "200", description = "返回信息", + content = @Content(mediaType = "application/json",schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "请求失败") + }) + @SystemControllerLog(description = "刪除模型数据") + @PostMapping("/model-data/deleteApi") + public Result > delete(@RequestBody @Valid DynamicDelete dto) { + try { + return Result.success(dynamicService.delete(dto)); + } catch (Exception e) { + log.error("deleteApi failed for table: {}", dto.getNameEn(), e); + return Result.failed("delete operation failed"); + } + } +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java b/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java new file mode 100644 index 00000000..fba8eac1 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java @@ -0,0 +1,107 @@ +package com.tinyengine.it.dynamic.dao; + +import org.apache.ibatis.jdbc.SQL; + +import java.util.List; +import java.util.Map; + +public class DynamicSqlProvider { + + public String select(Map params) { + String tableName = (String) params.get("tableName"); + List fields = (List) params.get("fields"); + Map conditions = (Map) params.get("conditions"); + Integer pageNum = (Integer) params.get("pageNum"); + Integer pageSize = (Integer) params.get("pageSize"); + String orderBy = (String) params.get("orderBy"); + String orderType = (String) params.get("orderType"); + + SQL sql = new SQL(); + + // 选择字段 + if (fields != null && !fields.isEmpty()) { + for (String field : fields) { + sql.SELECT(field); + } + } else { + sql.SELECT("*"); + } + + sql.FROM(tableName); + + // 条件 + if (conditions != null && !conditions.isEmpty()) { + for (Map.Entry entry : conditions.entrySet()) { + if (entry.getValue() != null) { + sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}"); + } + } + } + // 排序 + if (orderBy != null && !orderBy.isEmpty()) { + sql.ORDER_BY(orderBy + " " + orderType); + } + + // 分页 + if (pageNum != null && pageSize != null) { + return sql.toString() + " LIMIT " + (pageNum - 1) * pageSize + ", " + pageSize; + } + + return sql.toString(); + } + + public String insert(Map params) { + String tableName = (String) params.get("tableName"); + Map data = (Map) params.get("data"); + + SQL sql = new SQL(); + sql.INSERT_INTO(tableName); + + if (data != null && !data.isEmpty()) { + for (Map.Entry entry : data.entrySet()) { + sql.VALUES(entry.getKey(), "#{data." + entry.getKey() + "}"); + } + } + + return sql.toString(); + } + + public String update(Map params) { + String tableName = (String) params.get("tableName"); + Map data = (Map) params.get("data"); + Map conditions = (Map) params.get("conditions"); + + SQL sql = new SQL(); + sql.UPDATE(tableName); + + if (data != null && !data.isEmpty()) { + for (Map.Entry entry : data.entrySet()) { + sql.SET(entry.getKey() + " = #{data." + entry.getKey() + "}"); + } + } + + if (conditions != null && !conditions.isEmpty()) { + for (Map.Entry entry : conditions.entrySet()) { + sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}"); + } + } + + return sql.toString(); + } + + public String delete(Map params) { + String tableName = (String) params.get("tableName"); + Map conditions = (Map) params.get("conditions"); + + SQL sql = new SQL(); + sql.DELETE_FROM(tableName); + + if (conditions != null && !conditions.isEmpty()) { + for (Map.Entry entry : conditions.entrySet()) { + sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}"); + } + } + + return sql.toString(); + } +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dao/ModelDataDao.java b/base/src/main/java/com/tinyengine/it/dynamic/dao/ModelDataDao.java new file mode 100644 index 00000000..36f1f8c6 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dao/ModelDataDao.java @@ -0,0 +1,32 @@ +package com.tinyengine.it.dynamic.dao; + +import com.alibaba.fastjson.JSONObject; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; + +@Repository +@Mapper +public interface ModelDataDao { + + @SelectProvider(type = DynamicSqlProvider.class, method = "select") + List select(Map params); + + @InsertProvider(type = DynamicSqlProvider.class, method = "insert") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + Long insert(Map params); + + @UpdateProvider(type = DynamicSqlProvider.class, method = "update") + Integer update(Map params); + + @DeleteProvider(type = DynamicSqlProvider.class, method = "delete") + Integer delete(Map params); + + @Select("SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT " + + "FROM INFORMATION_SCHEMA.COLUMNS " + + "WHERE TABLE_NAME = #{tableName} AND TABLE_SCHEMA = DATABASE()") + List> getTableStructure(String tableName); +} + diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicDelete.java b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicDelete.java new file mode 100644 index 00000000..caaa574e --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicDelete.java @@ -0,0 +1,11 @@ +package com.tinyengine.it.dynamic.dto; + +import lombok.Data; + +import java.util.Map; + +@Data +public class DynamicDelete { + private String nameEn; + private Integer id; +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicInsert.java b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicInsert.java new file mode 100644 index 00000000..fd41ae74 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicInsert.java @@ -0,0 +1,10 @@ +package com.tinyengine.it.dynamic.dto; + +import lombok.Data; + +import java.util.Map; +@Data +public class DynamicInsert { + private String nameEn; + private Map params; // 插入/更新数据 +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java new file mode 100644 index 00000000..79608a70 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java @@ -0,0 +1,19 @@ +package com.tinyengine.it.dynamic.dto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class DynamicQuery { + + private String nameEn; // 表名 + private String nameCh; // 表中文名 + private List fields; // 查询字段 + private Map params; // 查询条件 + private Integer currentPage = 1; // 页码 + private Integer pageSize = 10; // 每页大小 + private String orderBy; // 排序字段 + private String orderType = "ASC"; // 排序方式 +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicUpdate.java b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicUpdate.java new file mode 100644 index 00000000..a67e8743 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicUpdate.java @@ -0,0 +1,11 @@ +package com.tinyengine.it.dynamic.dto; + +import lombok.Data; + +import java.util.Map; +@Data +public class DynamicUpdate { + private String nameEn; + private Map data; + private Map params;// 查询条件 +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java b/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java new file mode 100644 index 00000000..1b5a0546 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java @@ -0,0 +1,631 @@ +package com.tinyengine.it.dynamic.service; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.tinyengine.it.common.context.LoginUserContext; +import com.tinyengine.it.dynamic.dto.DynamicDelete; +import com.tinyengine.it.dynamic.dto.DynamicInsert; +import com.tinyengine.it.dynamic.dto.DynamicQuery; +import com.tinyengine.it.dynamic.dto.DynamicUpdate; +import com.tinyengine.it.model.dto.ParametersDto; +import com.tinyengine.it.model.entity.Model; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.sql.*; +import java.sql.Date; +import java.util.*; +import java.util.stream.Collectors; + + +@Service +@Slf4j +@RequiredArgsConstructor +public class DynamicModelService { + + private final JdbcTemplate jdbcTemplate; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final LoginUserContext loginUserContext; + + + /** + * 创建动态表 + */ + @Transactional + public void createDynamicTable(Model modelMetadata) { + String tableName = getTableName(modelMetadata.getNameEn()); + String sql = generateCreateTableSQL(tableName, modelMetadata); + + log.info("创建动态表SQL: \n{}", sql); + + try { + jdbcTemplate.execute(sql); + log.info("创建动态表成功: {}", tableName); + + } catch (Exception e) { + log.error("创建动态表失败: {}", tableName, e); + throw new RuntimeException("创建动态表失败: " + e.getMessage()); + } + } + private String generateDropTableSQL(String tableName) { + if (tableName == null || tableName.isEmpty()) { + throw new IllegalArgumentException("Table name cannot be null or empty"); + } + + // Validate table name to prevent SQL injection + if (!tableName.matches("^[a-zA-Z0-9_]+$")) { + throw new IllegalArgumentException("Invalid table name: " + tableName); + } + StringBuilder sql = new StringBuilder(); + sql.append("DROP TABLE IF EXISTS ").append(tableName).append(";"); + return sql.toString(); + } + + public void dropDynamicTable(Model modelMetadata) { + if (modelMetadata == null || modelMetadata.getNameEn() == null || modelMetadata.getNameEn().isEmpty()) { + throw new IllegalArgumentException("Model metadata or table name cannot be null or empty"); + } + String tableName = getTableName(modelMetadata.getNameEn()); + + String sql = generateDropTableSQL(tableName); + try { + jdbcTemplate.execute(sql); + log.info("Successfully dropped table: {}", tableName); + } catch (Exception e) { + log.error("Failed to drop table: {}", tableName, e); + throw new RuntimeException("Failed to drop table: " + tableName, e); + } + } + /** + * 生成创建表的SQL + */ + private String generateCreateTableSQL(String tableName, Model model) { + StringBuilder sql = new StringBuilder(); + sql.append("CREATE TABLE IF NOT EXISTS ").append(tableName).append(" (\n"); + + // 基础字段 + List columns = new ArrayList<>(); + columns.add("id INT PRIMARY KEY AUTO_INCREMENT"); + + + // 用户定义字段 + for (ParametersDto field : model.getParameters()) { + if(!Objects.equals(field.getProp(), "id")){ + String columnDef = generateColumnDefinition(field,"init"); + columns.add(columnDef); + } + } + // 基础字段 + columns.add("created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"); + columns.add("updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"); + columns.add("deleted_at TIMESTAMP NULL"); + columns.add("created_by INT NOT NULL"); + columns.add("updated_by INT NOT NULL"); + + + sql.append(String.join(",\n", columns)); + sql.append("\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); + + return sql.toString(); + } + public void initializeDynamicTable(Model model, Long userId) { + if (model == null || CollectionUtils.isEmpty(model.getParameters())) { + throw new IllegalArgumentException("Model or parameters cannot be null or empty"); + } + + String tableName = getTableName(model.getNameEn()); + List parameters = model.getParameters(); + + // Prepare columns and values + List columns = new ArrayList<>(); + List values = new ArrayList<>(); + + for (ParametersDto param : parameters) { + String columnName = param.getProp(); + String fieldType = param.getType(); + param.setDefaultValue("1"); + String value = param.getDefaultValue(); + + if (value == null && Boolean.TRUE.equals(param.getRequired())) { + throw new IllegalArgumentException("Missing required parameter defaultValue: " + columnName); + } + + columns.add(columnName); + values.add(convertValueByType(value, fieldType, columnName)); + } + + // Add system fields + columns.add("created_by"); + columns.add("updated_by"); + values.add(userId); + values.add(userId); + + // Construct SQL + String sql = String.format( + "INSERT INTO %s (%s) VALUES (%s)", + tableName, + String.join(", ", columns), + columns.stream().map(c -> "?").collect(Collectors.joining(", ")) + ); + + // Execute SQL + jdbcTemplate.update(sql, values.toArray()); + } + + + + + + /** + * 通用查询方法,避免TypeHandler冲突 + */ + public List> dynamicQuery(String tableName, + List fields, + Map conditions, + String orderBy, + Integer limit) { + + // 1. 构建SQL + StringBuilder sql = new StringBuilder("SELECT "); + + if (fields != null && !fields.isEmpty()) { + sql.append(String.join(", ", fields)); + } else { + sql.append("*"); + } + + sql.append(" FROM ").append(tableName); + + // 2. 构建WHERE条件 + if (conditions != null && !conditions.isEmpty()) { + List whereClauses = new ArrayList<>(); + boolean whereAdded = false; + getWhereCondition(conditions, sql, whereAdded, whereClauses); + } + + // 3. 排序 + if (orderBy != null && !orderBy.isEmpty()) { + sql.append(" ORDER BY ").append(orderBy); + } + + // 4. 分页 + if (limit != null && limit > 0) { + sql.append(" LIMIT ").append(limit); + } + + // 5. 执行查询 + if (conditions != null && !conditions.isEmpty()) { + return namedParameterJdbcTemplate.queryForList(sql.toString(), conditions); + } else { + return jdbcTemplate.queryForList(sql.toString()); + } + } + + public List> dynamicCount(String tableName, Map conditions) { + + // 1. 构建SQL + StringBuilder sql = new StringBuilder("SELECT COUNT(*) as count"); + + + + sql.append(" FROM ").append(tableName); + + // 2. 构建WHERE条件 + if (conditions != null && !conditions.isEmpty()) { + boolean whereAdded = false; + List whereClauses = new ArrayList<>(); + getWhereCondition(conditions, sql, whereAdded, whereClauses); + } + + // 5. 执行查询 + if (conditions != null && !conditions.isEmpty()) { + return namedParameterJdbcTemplate.queryForList(sql.toString(), conditions); + } else { + return jdbcTemplate.queryForList(sql.toString()); + } + } + + private void getWhereCondition(Map conditions, StringBuilder sql, boolean whereAdded, List whereClauses) { + for (Map.Entry entry : conditions.entrySet()) { + if (entry.getValue() != null) { + whereAdded = true; + whereClauses.add(entry.getKey() + " = :" + entry.getKey()); + } + } + if(whereAdded){ + sql.append(" WHERE "); + sql.append(String.join(" AND ", whereClauses)); + + } + } + + /** + * 查询总数 + */ + public Long count(String tableName, Map conditions) { + List> result = dynamicCount(tableName, conditions); + return Long.parseLong(result.get(0).get("count").toString()); + } + /** + * 分页查询 + */ + public Map queryWithPage(DynamicQuery dto) { + String tableName = getTableName( dto.getNameEn()); + List fields = dto.getFields(); + Map conditions = dto.getParams(); + String orderBy = dto.getOrderBy(); + Integer pageNum = dto.getCurrentPage(); + Integer pageSize = dto.getPageSize(); + + // 计算分页 + Integer limit = null; + if (pageNum != null && pageSize != null) { + limit = pageSize; + // 如果需要偏移量,可以在这里处理 + } + + // 执行查询 + List> data = dynamicQuery( + tableName, fields, conditions, orderBy, limit); + Long count = count(tableName, conditions); + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", data); + result.put("total", count); + + return result; + } + private Object convertValueByType(Object value, String fieldType, String columnName) { + try { + switch (fieldType) { + case "String": + return value != null ? value.toString() : null; + case "Number": + return value != null ? Integer.parseInt(value.toString()) : null; + case "Boolean": + return value != null ? Boolean.parseBoolean(value.toString()) : null; + case "Date": + return value !=null ? Date.valueOf(value.toString()):null; // Assume proper date formatting is handled elsewhere + case "DateTime": + return value; // Assume proper date formatting is handled elsewhere + case "Enum": + return value; // Validation for enums should be handled before this + default: + return value; + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid value for field: " + columnName, e); + } + } + @Transactional + public void modifyTableStructure(Model model) { + String tableName = getTableName(model.getNameEn()); + List parameters = model.getParameters(); + + + // Fetch existing table structure + String fetchColumnsSql = "SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"; + List> existingColumns = jdbcTemplate.query(fetchColumnsSql, new Object[]{tableName}, (rs, rowNum) -> { + Map column = new HashMap<>(); + column.put("COLUMN_NAME", rs.getString("COLUMN_NAME")); + column.put("DATA_TYPE", rs.getString("DATA_TYPE")); + return column; + }); + + Map existingColumnMap = existingColumns.stream() + .collect(Collectors.toMap(col -> col.get("COLUMN_NAME"), col -> col.get("DATA_TYPE"))); + + // Generate ALTER TABLE statements + List alterStatements = new ArrayList<>(); + // Add or modify columns based on parameters + for (int i = 0; i < parameters.size(); i++) { + String afterColumn = null; + if(i>0){ + afterColumn = parameters.get(i - 1).getProp(); + } + ParametersDto param = parameters.get(i); + String columnName = param.getProp(); + String columnType = mapJavaTypeToSQL(param.getType()); + + if (!existingColumnMap.containsKey(columnName)) { + // Add new column + String addSql = generateColumnDefinition(param,"add"); + if(afterColumn != null){ + addSql += " AFTER " + afterColumn; + } + alterStatements.add(addSql); + } else if (!existingColumnMap.get(columnName).equalsIgnoreCase(columnType)) { + // Modify existing column + alterStatements.add(String.format("MODIFY COLUMN %s %s", columnName, columnType)); + } + } + + addCommonFields(parameters); + + // Drop columns that are not in the parameters + for (String existingColumn : existingColumnMap.keySet()) { + if (parameters.stream().noneMatch(param -> param.getProp().equals(existingColumn))) { + alterStatements.add(String.format("DROP COLUMN %s", existingColumn)); + } + } + + // Execute ALTER TABLE statements + for (String alterStatement : alterStatements) { + String sql = String.format("ALTER TABLE %s %s", tableName, alterStatement); + jdbcTemplate.execute(sql); + } + } + private void addCommonFields(List parameters) { + ParametersDto id = new ParametersDto(); + id.setProp("id"); + parameters.add(id); + ParametersDto createdAt = new ParametersDto(); + createdAt.setProp("created_at"); + parameters.add(createdAt); + ParametersDto updatedAt = new ParametersDto(); + updatedAt.setProp("updated_at"); + parameters.add(updatedAt); + ParametersDto deletedAt = new ParametersDto(); + deletedAt.setProp("deleted_at"); + parameters.add(deletedAt); + ParametersDto createdBy = new ParametersDto(); + createdBy.setProp("created_by"); + parameters.add(createdBy); + ParametersDto updatedBy = new ParametersDto(); + updatedBy.setProp("updated_by"); + parameters.add(updatedBy); + } + private static String mapJavaTypeToSQL(String javaType) { + if (javaType == null) { + return "VARCHAR(255)"; // 默认处理 + } + switch (javaType) { + case "String": + return "VARCHAR"; + case "Number": + return "INT"; + case "Boolean": + return "TINYINT"; + case "Date": + return "TIMESTAMP"; + case "Enum": + return "Enum"; + default: + return "TEXT"; // 默认处理 + } + } + + + /** + * 生成字段定义 + */ + private String generateColumnDefinition(ParametersDto field,String type) { + StringBuilder sb = new StringBuilder(); + if(type.equals("add")) { + sb.append("ADD COLUMN "); + } else if(type.equals("modify")){ + sb.append("MODIFY COLUMN "); + } + sb.append(field.getProp()).append(" "); + + // 映射数据类型 + switch (field.getType()) { + case "String": + int maxLength = field.getMaxLength() != null ? field.getMaxLength() : 255; + sb.append("VARCHAR(").append(maxLength).append(")"); + break; + case "Integer", "Number": + sb.append("INT"); + break; + case "Boolean": + sb.append("TINYINT(1)"); + break; + case "Date": + sb.append("DATE"); + break; + case "DateTime": + sb.append("DATETIME"); + break; + case "Enum": + sb.append("ENUM").append("(").append(getEnumOptions(field.getOptions())).append(")"); + break; + default: + sb.append("TEXT"); + } + + if (Boolean.TRUE.equals(field.getRequired())) { + sb.append(" NOT NULL"); + } + + if (field.getDefaultValue() != null) { + sb.append(" DEFAULT '").append(field.getDefaultValue()).append("'"); + } + if(field.getDescription()!=null && !field.getDescription().isEmpty()){ + sb.append(" COMMENT '").append(field.getDescription()).append("'"); + } + + return sb.toString(); + } + + private String getEnumOptions(String optionStr) { + List options= new ArrayList<>(); + JSONArray jsonlist = JSON.parseArray(optionStr); + for (int i = 0; i < jsonlist.size(); i++) { + String value = jsonlist.getJSONObject(i).getString("value"); + options.add(value); + } + + return options.stream() + .map(opt -> "'" + opt + "'") + .collect(Collectors.joining(", ")); + } + + + /** + * 验证表和数据 + */ + private void validateTableAndData(String tableName, Map data) { + if (tableName == null || tableName.trim().isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + // 防止SQL注入,验证表名格式 + if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException("表名格式不正确"); + } + + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + + // 验证字段名格式 + for (String field : data.keySet()) { + if (!field.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException("字段名格式不正确: " + field); + } + } + } + + + /** + * 创建数据 + */ + public Map createData(DynamicInsert dataDto) { + + + String tableName = getTableName(dataDto.getNameEn()); + Map record = new HashMap<>(dataDto.getParams()); + String userId = loginUserContext.getLoginUserId(); + // 添加系统字段 + record.put("created_by",userId); + record.put("updated_by", userId); + + // 构建SQL + String columns = String.join(", ", record.keySet()); + String placeholders = record.keySet().stream() + .map(k -> "?") + .collect(Collectors.joining(", ")); + + String sql = String.format( + "INSERT INTO %s (%s) VALUES (%s)", + tableName, columns, placeholders + ); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(new PreparedStatementCreator() { + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + + int index = 1; + for (Object value : record.values()) { + ps.setObject(index++, value); + } + + return ps; + } + }, keyHolder); + + Long generatedId = keyHolder.getKey() != null ? keyHolder.getKey().longValue() : null; + + if (generatedId != null) { + record.put("id", generatedId); + } + + return record; + } + + + + + + /** + * 获取表名 + */ + private String getTableName(String modelId) { + return "dynamic_" + modelId.toLowerCase(); + } + + + + public Map getDataById(String modelId, Long id) { + String tableName = getTableName(modelId); + String sql = "SELECT * FROM " + tableName + " WHERE id = ?"; + + List> results = jdbcTemplate.queryForList(sql, id); + + if (results.isEmpty()) { + return null; + } else { + return results.get(0); + } + } + + public Map updateDateById(DynamicUpdate dto) { + String modelId = dto.getNameEn(); + Map params1 = dto.getParams(); + if(params1 == null || !params1.containsKey("id")) { + throw new IllegalArgumentException("更新操作必须指定ID"); + } + if(dto.getData() == null || dto.getData().isEmpty()) { + throw new IllegalArgumentException("更新操作必须指定更新数据"); + } + if(modelId == null || modelId.trim().isEmpty()) { + throw new IllegalArgumentException("模型ID不能为空"); + } + Long id = Long.parseLong(params1.get("id").toString()); + Map updateFields = dto.getData(); + String tableName = getTableName(modelId); + StringBuilder sql = new StringBuilder("UPDATE " + tableName + " SET "); + List params = new ArrayList<>(); + + for (Map.Entry entry : updateFields.entrySet()) { + sql.append(entry.getKey()).append(" = ?, "); + params.add(entry.getValue()); + } + + // 去掉最后的逗号和空格 + sql.setLength(sql.length() - 2); + sql.append(" WHERE id = ?"); + params.add(id); + + int rowsAffected = jdbcTemplate.update(sql.toString(), params.toArray()); + + Map result = new HashMap<>(); + result.put("rowsAffected", rowsAffected); + return result; + } + + public Map deleteDataById(DynamicDelete dto) { + String modelId = dto.getNameEn(); + if(modelId == null || modelId.trim().isEmpty()) { + throw new IllegalArgumentException("模型ID不能为空"); + } + if(dto.getId() == null) { + throw new IllegalArgumentException("删除操作必须指定ID"); + } + Long id = Long.valueOf(dto.getId()); + + String tableName = getTableName(modelId); + String sql = "DELETE FROM " + tableName + " WHERE id = ?"; + int update = jdbcTemplate.update(sql, id); + Map result = new HashMap<>(); + result.put("rowsAffected", update); + return result; + } + + + + + +} diff --git a/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java b/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java new file mode 100644 index 00000000..e04c5b14 --- /dev/null +++ b/base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java @@ -0,0 +1,231 @@ +package com.tinyengine.it.dynamic.service; + +import com.alibaba.fastjson.JSONObject; +import com.tinyengine.it.common.context.LoginUserContext; +import com.tinyengine.it.dynamic.dao.ModelDataDao; +import com.tinyengine.it.dynamic.dto.*; +import com.tinyengine.it.service.material.ModelService; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.util.*; +@Service +public class DynamicService { + @Autowired + private ModelDataDao dynamicDao; + @Autowired + private ModelService modelService; + @Autowired + private LoginUserContext loginUserContext; + + + // 操作类型常量 + private static final String OPERATION_SELECT = "SELECT"; + private static final String OPERATION_INSERT = "INSERT"; + private static final String OPERATION_UPDATE = "UPDATE"; + private static final String OPERATION_DELETE = "DELETE"; + + /** + * 查询数据 + * @param dto + * @return list + */ + public List query(DynamicQuery dto) { + String tableName = getTableName(dto.getNameEn()); + Map params = new HashMap<>(); + params.put("tableName", tableName); + params.put("fields", dto.getFields()); + params.put("conditions", dto.getParams()); + params.put("pageNum", dto.getCurrentPage()); + params.put("pageSize", dto.getPageSize()); + params.put("orderBy", dto.getOrderBy()); + params.put("orderType", dto.getOrderType()); + + return dynamicDao.select(params); + } + + /** + * 统计数量 + * @param tableName + * @param conditions + * @return long + */ + public Long count(String tableName, Map conditions) { + Map params = new HashMap<>(); + params.put("tableName", tableName); + params.put("fields", Arrays.asList("COUNT(*) as count")); + params.put("conditions", conditions); + + List result = dynamicDao.select(params); + return Long.parseLong(result.get(0).get("count").toString()); + } + + /** + * 分页查询 + * @param dto + * @return map + */ + public Map queryWithPage(DynamicQuery dto) { + if( dto.getNameEn() == null || dto.getNameEn().trim().isEmpty()) { + throw new IllegalArgumentException("查询操作必须指定模型名称"); + } + if( dto.getCurrentPage() == null || dto.getCurrentPage() <= 0) { + dto.setCurrentPage(1); + } + if( dto.getPageSize() == null || dto.getPageSize() <= 0) { + dto.setPageSize(10); + } + validateTableExists(dto.getNameEn()); + validateTableAndData(dto.getNameEn(), dto.getParams()); + List list = query(dto); + String tableName = getTableName(dto.getNameEn()); + Long total = count(tableName, dto.getParams()); + + Map result = new HashMap<>(); + result.put("list", list); + result.put("total", total); + result.put("pageNum", dto.getCurrentPage()); + result.put("pageSize", dto.getPageSize()); + result.put("pages", (int) Math.ceil((double) total / dto.getPageSize())); + + return result; + } + + /** + * 插入数据 + * @param dto + * @return map + */ + @Transactional + public Map insert(DynamicInsert dto) { + if( dto.getNameEn() == null || dto.getNameEn().trim().isEmpty()) { + throw new IllegalArgumentException("插入操作必须指定模型名称"); + } + if( dto.getParams() == null || dto.getParams().isEmpty()) { + throw new IllegalArgumentException("插入数据不能为空"); + } + validateTableExists(dto.getNameEn()); + validateTableAndData(dto.getNameEn(), dto.getParams()); + String tableName = getTableName(dto.getNameEn()); + Map params = new HashMap<>(); + params.put("tableName", tableName); + params.put("data", dto.getParams()); + String userId = loginUserContext.getLoginUserId(); + // 添加系统字段 + dto.getParams().put("created_by", userId); + dto.getParams().put("updated_by", userId); + + Map result = new HashMap<>(); + Long insertRow = dynamicDao.insert(params); + BigInteger id = (BigInteger) params.get("id"); + result.put("insert", insertRow); + result.put("id", id.longValue()); + return result; + } + + + /** + * 更新数据 + * @param dto + * @return + */ + @Transactional + public Map update(DynamicUpdate dto) { + if( dto.getNameEn() == null || dto.getNameEn().trim().isEmpty()) { + throw new IllegalArgumentException("更新操作必须指定模型名称"); + } + if (dto.getParams() == null || dto.getParams().isEmpty()) { + throw new IllegalArgumentException("更新操作必须指定条件"); + } + if( dto.getData() == null || dto.getData().isEmpty()) { + throw new IllegalArgumentException("更新数据不能为空"); + } + validateTableExists(dto.getNameEn()); + validateTableAndData(dto.getNameEn(), dto.getData()); + String tableName = getTableName(dto.getNameEn()); + Map params = new HashMap<>(); + params.put("tableName", tableName); + params.put("data", dto.getData()); + params.put("conditions", dto.getParams()); + Map result = new HashMap<>(); + Integer update = dynamicDao.update(params); + result.put("update", update); + return result; + } + + /** + * 删除数据 + */ + @Transactional + public Map delete(DynamicDelete dto) { + if( dto.getNameEn() == null || dto.getNameEn().trim().isEmpty()) { + throw new IllegalArgumentException("删除操作必须指定模型名称"); + } + if (dto.getId() == null ) { + throw new IllegalArgumentException("删除操作必须指定id"); + } + validateTableExists(dto.getNameEn()); + String tableName = getTableName(dto.getNameEn()); + + Map params = new HashMap<>(); + Map conditions = new HashMap<>(); + conditions.put("id", dto.getId()); + params.put("tableName", tableName); + params.put("conditions",conditions); + Map result = new HashMap<>(); + Integer delete = dynamicDao.delete(params); + result.put("delete", delete); + return result; + } + + + + /** + * 获取表结构 + */ + public List> getTableStructure(String tableName) { + validateTableExists(tableName); + return dynamicDao.getTableStructure(tableName); + } + + /** + * 验证表和数据 + */ + private void validateTableAndData(String tableName, Map data) { + if (tableName == null || tableName.trim().isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + // 防止SQL注入,验证表名格式 + if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException("表名格式不正确"); + } + + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("数据不能为空"); + } + + // 验证字段名格式 + for (String field : data.keySet()) { + if (!field.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { + throw new IllegalArgumentException("字段名格式不正确: " + field); + } + } + } + /** + * 验证表是否存在 + */ + public void validateTableExists(String tableName) { + List tables = modelService.getAllModelName(); + if (!tables.contains(tableName)) { + throw new IllegalArgumentException("模型不存在: " + tableName); + } + } + private String getTableName(String modelId) { + return "dynamic_" + modelId.toLowerCase(); + } + + +} diff --git a/base/src/main/java/com/tinyengine/it/model/dto/ParametersDto.java b/base/src/main/java/com/tinyengine/it/model/dto/ParametersDto.java index 680d3212..db97b37e 100644 --- a/base/src/main/java/com/tinyengine/it/model/dto/ParametersDto.java +++ b/base/src/main/java/com/tinyengine/it/model/dto/ParametersDto.java @@ -29,4 +29,5 @@ public class ParametersDto { private String options; private Boolean required; private String description; + private Integer maxLength; } diff --git a/base/src/main/java/com/tinyengine/it/service/material/ModelService.java b/base/src/main/java/com/tinyengine/it/service/material/ModelService.java index 8f88eaf1..6640b5f6 100644 --- a/base/src/main/java/com/tinyengine/it/service/material/ModelService.java +++ b/base/src/main/java/com/tinyengine/it/service/material/ModelService.java @@ -79,4 +79,10 @@ public interface ModelService extends IService{ * @return 拼接好的SQL语句字符串,每个表的SQL用分号分隔并换行 */ String getAllTable(); + + /** + * 获取所有模型名称列表 + * @return 模型名称列表 + */ + List getAllModelName(); } diff --git a/base/src/main/java/com/tinyengine/it/service/material/impl/ModelServiceImpl.java b/base/src/main/java/com/tinyengine/it/service/material/impl/ModelServiceImpl.java index 96bf4252..1a768b0d 100644 --- a/base/src/main/java/com/tinyengine/it/service/material/impl/ModelServiceImpl.java +++ b/base/src/main/java/com/tinyengine/it/service/material/impl/ModelServiceImpl.java @@ -20,6 +20,7 @@ import com.tinyengine.it.common.exception.ServiceException; import com.tinyengine.it.common.log.SystemServiceLog; import com.tinyengine.it.common.utils.JsonUtils; +import com.tinyengine.it.dynamic.service.DynamicModelService; import com.tinyengine.it.mapper.ModelMapper; import com.tinyengine.it.model.dto.MethodDto; import com.tinyengine.it.model.dto.ParametersDto; @@ -28,18 +29,21 @@ import com.tinyengine.it.model.entity.Model; import com.tinyengine.it.service.material.ModelService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.StringJoiner; +import java.util.*; import java.util.stream.Collectors; @Service @Slf4j public class ModelServiceImpl extends ServiceImpl implements ModelService { + + @Autowired + private DynamicModelService dynamicModelService; /** * 查询表t_model信息 * @@ -104,7 +108,16 @@ public Page pageQuery(int currentPage, int pageSize, String nameCn, Strin */ @Override @SystemServiceLog(description = "创建model实现方法") + @Transactional public Model createModel(Model model) { + // 验证模型唯一性 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("name_cn", model.getNameCn()) + .or() + .eq("name_en", model.getNameEn()); + if (this.baseMapper.selectCount(queryWrapper) > 0) { + throw new ServiceException(ExceptionEnum.CM003.getResultCode(), "Model with the same name already exists"); + } List methodDtos = new ArrayList<>(); methodDtos.add(getMethodDto(Enums.methodName.CREATED.getValue(), Enums.methodName.INSERTAPI.getValue(), model)); methodDtos.add(getMethodDto(Enums.methodName.UPDATE.getValue(), Enums.methodName.UPDATEAPI.getValue(), model)); @@ -115,6 +128,11 @@ public Model createModel(Model model) { if (result != 1) { throw new ServiceException(ExceptionEnum.CM001.getResultCode(), ExceptionEnum.CM001.getResultCode()); } + // 创建动态表 + dynamicModelService.createDynamicTable(model); + //初始化模型表數據 + // dynamicModelService.initializeDynamicTable(model, Long.valueOf(model.getCreatedBy())); + return model; } @@ -127,12 +145,19 @@ public Model createModel(Model model) { */ @Override @SystemServiceLog(description = "根据id删除model实现方法") + @Transactional public Model deleteModelById(Integer id) { Model model = this.baseMapper.selectById(id); int result = this.baseMapper.deleteById(id); if (result != 1) { throw new ServiceException(ExceptionEnum.CM001.getResultCode(), ExceptionEnum.CM001.getResultCode()); } + try { + dynamicModelService.dropDynamicTable(model); + } catch (Exception e) { + log.error("刪除动态表失败", e); + throw new RuntimeException("刪除动态表失败: " + e.getMessage()); + } return model; } @@ -145,6 +170,7 @@ public Model deleteModelById(Integer id) { */ @Override @SystemServiceLog(description = "根据id修改model实现方法") + @Transactional public Model updateModelById(Model model) { List methodDtos = new ArrayList<>(); methodDtos.add(getMethodDto(Enums.methodName.CREATED.getValue(), Enums.methodName.INSERTAPI.getValue(), model)); @@ -159,6 +185,15 @@ public Model updateModelById(Model model) { if (result != 1) { throw new ServiceException(ExceptionEnum.CM001.getResultCode(), ExceptionEnum.CM001.getResultCode()); } + + + // 修改动态表 + try { + dynamicModelService.modifyTableStructure(model); + } catch (Exception e) { + log.error("修改动态表失败", e); + throw new RuntimeException("修改动态表失败: " + e.getMessage()); + } Model modelResult = this.baseMapper.selectById(model.getId()); return modelResult; } @@ -209,6 +244,22 @@ public String getAllTable() { return sqlJoiner.toString(); } + /** + * 获取所有模型名称列表 + * + * @return 模型名称列表 + */ + @Override + public List getAllModelName() { + List modelList = this.baseMapper.selectList(null); + if (!CollectionUtils.isEmpty(modelList)) { + return modelList.stream() + .map(Model::getNameEn) + .collect(Collectors.toList()); + } + return null; + } + private String getTableByModle(Model model) { List rawList = model.getParameters(); List fields = rawList.stream() @@ -272,6 +323,10 @@ private MethodDto getMethodDto(String name, String nameEn, Model model) { requestParameter.setProp(Enums.methodParam.ID.getValue()); requestParameter.setType(Enums.paramType.NUMBER.getValue()); List parameterList = new ArrayList<>(); + RequestParameter requestNameEn = new RequestParameter(); + requestNameEn.setProp(Enums.methodParam.NAMEEN.getValue()); + requestNameEn.setType(Enums.paramType.STRING.getValue()); + parameterList.add(requestNameEn); if (name.equals(Enums.methodName.QUERY.getValue())) { RequestParameter currentPage = new RequestParameter(); currentPage.setProp(Enums.methodParam.CURRENTPAGE.getValue()); @@ -282,21 +337,24 @@ private MethodDto getMethodDto(String name, String nameEn, Model model) { RequestParameter nameCn = new RequestParameter(); nameCn.setProp(Enums.methodParam.NAMECN.getValue()); nameCn.setType(Enums.paramType.STRING.getValue()); - RequestParameter requestNameEn = new RequestParameter(); - requestNameEn.setProp(Enums.methodParam.NAMEEN.getValue()); - requestNameEn.setType(Enums.paramType.STRING.getValue()); parameterList.add(currentPage); parameterList.add(pageSize); parameterList.add(nameCn); - parameterList.add(requestNameEn); } + if( name.equals(Enums.methodName.UPDATE.getValue())) { + RequestParameter requestParameterData = new RequestParameter(); + requestParameterData.setProp(Enums.methodParam.DATA.getValue()); + requestParameterData.setType(Enums.paramType.OBJECT.getValue()); + requestParameterData.setChildren(model.getParameters()); + parameterList.add(requestParameterData); + } if (!name.equals(Enums.methodName.DELETE.getValue())) { - requestParameter.setProp(Enums.methodParam.PARAMS.getValue()); - requestParameter.setType(Enums.paramType.OBJECT.getValue()); - requestParameter.setChildren(model.getParameters()); - parameterList.add(requestParameter); - + RequestParameter requestParameterparams = new RequestParameter(); + requestParameterparams.setProp(Enums.methodParam.PARAMS.getValue()); + requestParameterparams.setType(Enums.paramType.OBJECT.getValue()); + requestParameterparams.setChildren(model.getParameters()); + parameterList.add(requestParameterparams); methodDto.setRequestParameters(parameterList); methodDto.setResponseParameters(responseParameterList); return methodDto;