Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object>> 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<Map<String, Object> > 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<Map<String, Object> > 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<Map<String, Object> > 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> params) {
String tableName = (String) params.get("tableName");
List<String> fields = (List<String>) params.get("fields");
Map<String, Object> conditions = (Map<String, Object>) 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<String, Object> 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

分页参数要做下校验

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已赋值默认值

}

return sql.toString();
}
Comment on lines +10 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: SQL injection vulnerability in select method.

Multiple inputs are directly interpolated into the SQL string without parameterization:

  1. tableName (line 30) - directly passed to sql.FROM()
  2. fields (lines 23-24) - directly passed to sql.SELECT()
  3. orderBy and orderType (line 42) - directly concatenated
  4. Column names in WHERE clause (line 36) - directly concatenated

While DynamicService.validateTableAndData() validates some inputs, this provider can be called independently and the validation doesn't cover orderBy, orderType, or field names. An attacker could inject malicious SQL through these vectors.

Proposed mitigation

Add validation within the provider itself for defense in depth:

 public String select(Map<String, Object> params) {
     String tableName = (String) params.get("tableName");
+    validateIdentifier(tableName, "tableName");
     List<String> fields = (List<String>) params.get("fields");
     Map<String, Object> conditions = (Map<String, Object>) 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) {
+            validateIdentifier(field, "field");
             sql.SELECT(field);
         }
     } else {
         sql.SELECT("*");
     }
     // ...
     // 排序
     if (orderBy != null && !orderBy.isEmpty()) {
+        validateIdentifier(orderBy, "orderBy");
+        if (!"ASC".equalsIgnoreCase(orderType) && !"DESC".equalsIgnoreCase(orderType)) {
+            throw new IllegalArgumentException("Invalid orderType: " + orderType);
+        }
         sql.ORDER_BY(orderBy + " " + orderType);
     }
     // ...
 }
 
+private void validateIdentifier(String identifier, String paramName) {
+    if (identifier == null || !identifier.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
+        throw new IllegalArgumentException("Invalid " + paramName + ": " + identifier);
+    }
+}
🤖 Prompt for AI Agents
In `@base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java`
around lines 10 - 51, The select method in DynamicSqlProvider currently
concatenates untrusted inputs (tableName, fields, orderBy, orderType, and
condition keys) into SQL, enabling injection; update select to validate and
whitelist identifiers before using them: ensure tableName and each field in
fields match a safe identifier whitelist or strict regex (letters, digits,
underscore) or against allowed table/column lists, validate orderBy is a single
whitelisted column and orderType is only "ASC" or "DESC", and validate
pageNum/pageSize are non-negative integers; only after validation pass the
sanitized identifiers to SQL.SELECT, SQL.FROM and SQL.ORDER_BY, and keep
parameter binding (#{conditions.xxx}) for values rather than concatenation to
eliminate injection vectors in condition values and LIMIT.


public String insert(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
Map<String, Object> data = (Map<String, Object>) params.get("data");

SQL sql = new SQL();
sql.INSERT_INTO(tableName);

if (data != null && !data.isEmpty()) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
sql.VALUES(entry.getKey(), "#{data." + entry.getKey() + "}");
}
}

return sql.toString();
}

public String update(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
Map<String, Object> data = (Map<String, Object>) params.get("data");
Map<String, Object> conditions = (Map<String, Object>) params.get("conditions");

SQL sql = new SQL();
sql.UPDATE(tableName);

if (data != null && !data.isEmpty()) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
sql.SET(entry.getKey() + " = #{data." + entry.getKey() + "}");
}
}

if (conditions != null && !conditions.isEmpty()) {
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
}
}

return sql.toString();
}
Comment on lines +69 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SQL injection risk in update method.

Same vulnerability pattern: tableName and column names from data and conditions maps are directly interpolated. Ensure all identifiers are validated before SQL construction.

🤖 Prompt for AI Agents
In `@base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java`
around lines 69 - 90, The update method in DynamicSqlProvider currently
interpolates tableName and column names from the data and conditions maps
directly (variables: tableName, data, conditions in method update of class
DynamicSqlProvider), creating an SQL injection risk; fix by
validating/whitelisting the tableName and each column key before building the
SQL (e.g., check against an allow-list or strict identifier regex like
/^[A-Za-z_][A-Za-z0-9_]*$/) and throw an IllegalArgumentException for any
invalid identifier, or resolve identifiers via a known mapping of
logical->actual column names; keep using parameter placeholders (#{data.xxx},
#{conditions.xxx}) for values but do not concatenate unchecked identifiers into
sql.SET/sql.WHERE.


public String delete(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
Map<String, Object> conditions = (Map<String, Object>) params.get("conditions");

SQL sql = new SQL();
sql.DELETE_FROM(tableName);

if (conditions != null && !conditions.isEmpty()) {
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
}
}

return sql.toString();
}
Comment on lines +92 to +106
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SQL injection risk in delete method - potential for mass deletion.

The delete method has no safeguard against empty conditions. If conditions is null or empty, this generates DELETE FROM tableName without a WHERE clause, potentially deleting all rows.

Proposed safeguard
 public String delete(Map<String, Object> params) {
     String tableName = (String) params.get("tableName");
     Map<String, Object> conditions = (Map<String, Object>) params.get("conditions");
 
+    if (conditions == null || conditions.isEmpty()) {
+        throw new IllegalArgumentException("Delete operation requires conditions");
+    }
+
     SQL sql = new SQL();
     sql.DELETE_FROM(tableName);
 
-    if (conditions != null && !conditions.isEmpty()) {
-        for (Map.Entry<String, Object> entry : conditions.entrySet()) {
-            sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
-        }
+    for (Map.Entry<String, Object> entry : conditions.entrySet()) {
+        sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
     }
 
     return sql.toString();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String delete(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
Map<String, Object> conditions = (Map<String, Object>) params.get("conditions");
SQL sql = new SQL();
sql.DELETE_FROM(tableName);
if (conditions != null && !conditions.isEmpty()) {
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
}
}
return sql.toString();
}
public String delete(Map<String, Object> params) {
String tableName = (String) params.get("tableName");
Map<String, Object> conditions = (Map<String, Object>) params.get("conditions");
if (conditions == null || conditions.isEmpty()) {
throw new IllegalArgumentException("Delete operation requires conditions");
}
SQL sql = new SQL();
sql.DELETE_FROM(tableName);
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
}
return sql.toString();
}
🤖 Prompt for AI Agents
In `@base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java`
around lines 92 - 106, The delete(Map<String, Object> params) method currently
issues DELETE_FROM(tableName) even when conditions is null/empty; add a guard
that checks the conditions Map (from params.get("conditions")) and if it is null
or empty throw an IllegalArgumentException (or another explicit runtime
exception) instead of building the SQL, so no SQL is generated without a WHERE;
update the method that builds SQL (uses SQL sql = new SQL();
sql.DELETE_FROM(tableName); sql.WHERE(...)) to only perform DELETE_FROM and add
WHERE clauses after the non-empty check (or return/throw before calling
sql.DELETE_FROM) to ensure you never produce a DELETE FROM without conditions.

}
32 changes: 32 additions & 0 deletions base/src/main/java/com/tinyengine/it/dynamic/dao/ModelDataDao.java
Original file line number Diff line number Diff line change
@@ -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<JSONObject> select(Map<String, Object> params);

@InsertProvider(type = DynamicSqlProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
Long insert(Map<String, Object> params);
Comment on lines +17 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect return type for insert method.

The @Options(useGeneratedKeys = true, keyProperty = "id") annotation populates the generated key into the params map, but the method return type is Long. MyBatis insert methods return the number of affected rows (typically 1), not the generated key. The caller in DynamicService.insert() correctly retrieves the ID from params.get("id"), but the return value insertRow is misleadingly named and typed.

Proposed fix
 	`@InsertProvider`(type = DynamicSqlProvider.class, method = "insert")
 	`@Options`(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
-	Long insert(Map<String, Object> params);
+	int insert(Map<String, Object> params);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@InsertProvider(type = DynamicSqlProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
Long insert(Map<String, Object> params);
`@InsertProvider`(type = DynamicSqlProvider.class, method = "insert")
`@Options`(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insert(Map<String, Object> params);
🤖 Prompt for AI Agents
In `@base/src/main/java/com/tinyengine/it/dynamic/dao/ModelDataDao.java` around
lines 17 - 19, The DAO insert method has the wrong return type: change
ModelDataDao.insert's signature from Long insert(Map<String,Object> params) to
int (or Integer) insert(Map<String,Object> params) because MyBatis returns the
number of affected rows while generated key is populated into params via
`@Options`; then update callers (e.g., DynamicService.insert) to treat the return
value as rowsAffected (rename insertRow -> rowsAffected) and continue to read
the generated id from params.get("id"); leave DynamicSqlProvider.insert and the
`@Options` annotation as-is.


@UpdateProvider(type = DynamicSqlProvider.class, method = "update")
Integer update(Map<String, Object> params);

@DeleteProvider(type = DynamicSqlProvider.class, method = "delete")
Integer delete(Map<String, Object> params);

@Select("SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT " +
"FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_NAME = #{tableName} AND TABLE_SCHEMA = DATABASE()")
List<Map<String, Object>> getTableStructure(String tableName);
}

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> params; // 插入/更新数据
}
19 changes: 19 additions & 0 deletions base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java
Original file line number Diff line number Diff line change
@@ -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<String> fields; // 查询字段
private Map<String, Object> params; // 查询条件
private Integer currentPage = 1; // 页码
private Integer pageSize = 10; // 每页大小
private String orderBy; // 排序字段
private String orderType = "ASC"; // 排序方式
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> data;
private Map<String, Object> params;// 查询条件
}
Loading
Loading