package com.kucun.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.kucun.Service.AppService; import com.kucun.Service.DynamicRepositoryService; import com.kucun.data.entity.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; import java.util.Date; import java.util.List; import java.util.Map; import javax.validation.Valid; @RestController @RequestMapping("/app") public class AppController { @Autowired private AppService appService; @Autowired private ObjectMapper objectMapper; private Map<String, Class<?>> ENTITY_MAP ; @Autowired public AppController( DynamicRepositoryService dynamicRepositoryService, AppService appService, ObjectMapper objectMapper ) { this.ENTITY_MAP = dynamicRepositoryService.getStringClassMap(); this.appService = appService; this.objectMapper = objectMapper; } // ====================== 数据查询 ====================== @GetMapping("/all") public Information getAllData( @RequestParam(value = "since",required=false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date since ) { System.out.println("since:"+since); try { if(since == null) return appService.getAllData(); else return appService.getUpdatesSince(since); } catch (Exception e) { // TODO: handle exception return Information.NewFail(e.getMessage()); } } // 添加保存全部数据的API端点 @PostMapping("/save-all") public Information saveAllData(@RequestBody Map<String, List<?>> allData) { try { return appService.saveAllData(allData); } catch (Exception e) { return Information.NewFail("保存全部数据失败: " + e.getMessage()); } } @GetMapping("/all/{entityType}") public Information getEntityData(@PathVariable String entityType, @RequestParam(value = "since",required=false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date since) { return appService.getEntityData(entityType.toLowerCase()); } // ====================== CRUD操作 ====================== @PostMapping("/add/{entityType}") public Information addEntity( @PathVariable String entityType, @RequestBody @Valid Map<String, Object> requestBody ) { return handleEntityOperation(entityType, requestBody, "add"); } @PostMapping("/select/{entityType}") public Information queryEntity( @PathVariable String entityType, @RequestBody Map<String, Object> requestBody ) { return handleEntityOperation(entityType, requestBody, "select"); } @PostMapping("/delete/{entityType}") public Information deleteEntity( @PathVariable String entityType, @RequestBody Map<String, Object> requestBody ) { return handleEntityOperation(entityType, requestBody, "delete"); } @PostMapping("/update/{entityType}") public Information updateEntity( @PathVariable String entityType, @RequestBody Map<String, Object> requestBody ) { return handleEntityOperation(entityType, requestBody, "update"); } // ====================== 核心辅助方法 ====================== private Information handleEntityOperation( String entityType, Map<String, Object> requestBody, String operation ) { String normalizedType = entityType.toLowerCase(); // 特殊处理 Dingdan_bancai if ("dingdan_bancai".equals(normalizedType)) { try { Dingdan_bancai entity = objectMapper.convertValue(requestBody, Dingdan_bancai.class); // 从请求体中提取用户ID并设置到临时字段 if (requestBody.containsKey("currentUserId")) { entity.setCurrentUserId(((Integer) requestBody.get("currentUserId"))); } return appService.handleDingdanBancaiOperation( entity, operation); } catch (Exception e) { return Information.NewFail(operation + "操作失败: " + e.getMessage()); } } Class<?> entityClass = ENTITY_MAP.get(normalizedType); if (entityClass == null) { return Information.NewFail("不支持的实体类型: " + entityType); } try { Object entity = objectMapper.convertValue(requestBody, entityClass); //System.out.println(Information.NewSuccess(requestBody).DataJson()); switch (operation) { case "add": return appService.addEntity(entity); case "select": return appService.queryEntity((EntityBasis) entity); case "delete": // 确保实体实现了EntityBasis接口 if (entity instanceof EntityBasis) { return appService.deleteEntity((EntityBasis) entity); } else { return Information.NewFail("删除操作需要实体实现EntityBasis接口"); } case "update": return appService.updateEntity(entity); default: return Information.NewFail("无效的操作类型"); } } catch (Exception e) { return Information.NewFail(operation + "操作失败: " + e.getMessage()); } } }Request URL: http://192.168.1.4:8080/app/save-all Request Method: POST Status Code: 404 Remote Address: 192.168.1.4:8080 Referrer Policy: strict-origin-when-cross-origin Connection: keep-alive Content-Language: zh-CN Content-Type: application/json Date: Thu, 31 Jul 2025 17:01:14 GMT Keep-Alive: timeout=60 Transfer-Encoding: chunked Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: keep-alive Content-Length: 76274 content-type: application/json Host: 192.168.1.4:8080 Referer: https://servicewechat.com/wxfb2de4d38511c8cf/devtools/page-frame.html User-Agent: Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Mobile Safari/537.36 wechatdevtools/1.06.2504010 MicroMessenger/8.0.5 Language/zh_CN webview/ sessionid/14 {bancais: [{caizhi: {id: 1}, mupi1: {id: 1}, mupi2: {id: 1}, houdu: 15, kucun: null, id: 1,…},…],…} bancais: [{caizhi: {id: 1}, mupi1: {id: 1}, mupi2: {id: 1}, houdu: 15, kucun: null, id: 1,…},…] caizhis: [{name: "纤维板", id: 1, lastUpdated: "2025-06-29T19:58:33.000+00:00", deleted: false, deletedAt: null},…] chanpin_zujians: [{chanpin: {id: 1}, zujian: {id: 1}, bancai: {id: 1}, one_howmany: 20, zujianshu: null, id: 1,…},…] chanpins: [{dingdan_chanpin: [{id: 10}, {id: 14}], bianhao: "AN-1210", chanpin_zujian: [{id: 1}], id: 1,…},…] dingdan_bancais: [{id: 1, lastUpdated: "2025-06-29T13:50:38.000+00:00", deleted: false, deletedAt: null,…},…] dingdan_chanpins: [{dingdan: {id: 3}, chanpin: {id: 3}, shuliang: 200, id: 5,…},…] dingdans: [,…] jinhuos: [{dingdan_bancai: {id: 2}, shuliang: 42, date: "2025-07-01T07:47:35.000+00:00", user: {id: 1},…},…] kucuns: [,…] mupis: [{you: true, name: "桃花心", id: 1, lastUpdated: "2025-06-29T19:58:37.000+00:00", deleted: false,…},…] users: [,…] zujians: [{name: "侧板", chanping_zujian: [{id: 1}, {id: 2}, {id: 3}], id: 1,…},…] _lastModified: null _lastSync: "2025-07-31T17:01:10.572Z" 2025-08-01 01:01:29.902 INFO 19848 --- [io-8080-exec-10] com.kucun.aspect.LoggingAspect : Request URL: http://192.168.1.4:8080/app/save-all 2025-08-01 01:01:29.902 INFO 19848 --- [io-8080-exec-10] com.kucun.aspect.LoggingAspect : Request Method: POST 2025-08-01 01:01:29.903 INFO 19848 --- [io-8080-exec-10] com.kucun.aspect.LoggingAspect : Request Parameters: {"requestBody":"{}"} 2025-08-01 01:01:29.903 INFO 19848 --- [io-8080-exec-10] com.kucun.aspect.LoggingAspect : Response JSON: error
时间: 2025-08-01 22:24:10 浏览: 20
<think>我们之前已经分析过,问题在于`saveAllData`方法的参数类型为`Map<String, List<?>>`,但实际传入的JSON是一个包含多个键值对的对象,每个键对应的值是一个数组(列表)。然而,由于JSON结构的复杂性(例如,数组中的元素是对象,并且这些对象可能包含嵌套对象),Spring在尝试将请求体映射到`Map<String, List<?>>`时可能会失败,导致请求体被解析为空(即`{}`),从而引发404错误(因为实际上没有找到匹配的方法,参数类型不匹配会导致Spring认为该方法不能处理该请求)。
解决方案:
1. 修改`saveAllData`方法的参数类型为`Map<String, Object>`,然后在服务层中根据键名获取对应的列表(每个列表元素是Map,然后转换为对应的实体类)。
2. 或者,我们可以使用一个自定义的DTO类来接收整个JSON,但考虑到数据结构是动态的(键名对应不同的实体列表),使用Map更灵活。
我们已经尝试过将参数改为`Map<String, Object>`,但是日志显示请求体仍然为空(`{}`),这可能是由于其他原因导致的,例如:
- 请求体过大,被截断?
- 日志记录方式问题,没有打印出完整的请求体?
- 客户端发送的请求格式不正确?
但是,根据日志`Request Parameters: {"requestBody":"{}"}`,说明请求体被解析为空。这可能是由于请求体在传输过程中出现了问题,或者Spring的HttpMessageConverter无法解析请求体。
另一种可能:客户端发送的请求头`Content-Type`不是`application/json`,或者字符编码问题。
但是,根据您提供的请求头信息:
content-type: application/json
Content-Length: 76274
所以,请求头是正确的。
我们注意到请求体很大(76274字节),可能是由于请求体过大,导致在解析过程中出现问题。但是,Spring Boot默认对请求体大小没有限制(除非自己配置了限制)。
因此,我们尝试另一种方法:将参数类型改为`String`,然后手动解析JSON。这样可以避免Spring自动转换失败的问题。
修改`saveAllData`方法如下:
```java
@PostMapping("/save-all")
public Information saveAllData(@RequestBody String allDataJson) {
try {
// 使用Jackson将JSON字符串解析为Map
Map<String, Object> allData = objectMapper.readValue(allDataJson, Map.class);
return appService.saveAllData(allData);
} catch (Exception e) {
return Information.NewFail("保存全部数据失败: " + e.getMessage());
}
}
```
同时,修改`AppService`中的`saveAllData`方法,使其接收`Map<String, Object>`参数。
这样修改后,我们手动将JSON字符串解析为Map,可以避免Spring自动转换失败的问题。
另外,我们还需要检查`AppService`中的`saveAllData`方法实现,确保它能够处理这个Map。例如,对于每个实体类型,我们获取对应的列表,然后遍历列表,将每个元素(Map)转换为对应的实体对象,并保存。
示例代码(在AppService中):
```java
public Information saveAllData(Map<String, Object> allData) {
try {
// 遍历所有键
for (Map.Entry<String, Object> entry : allData.entrySet()) {
String entityType = entry.getKey();
Object value = entry.getValue();
// 只处理列表类型的数据
if (value instanceof List) {
List<?> list = (List<?>) value;
// 根据entityType获取对应的实体类
Class<?> entityClass = ENTITY_MAP.get(entityType.toLowerCase());
if (entityClass != null) {
for (Object item : list) {
// 将item(实际上是一个Map)转换为实体对象
Object entity = objectMapper.convertValue(item, entityClass);
// 保存实体
// 注意:这里需要根据实际情况调用repository保存
// 例如:使用对应的repository保存实体
// 由于我们不知道具体的repository,这里需要动态获取或者使用一个通用的方法
// 这里假设有一个通用的saveEntity方法,需要自己实现
saveEntity(entity);
}
}
}
}
return Information.NewSuccess("数据保存成功");
} catch (Exception e) {
return Information.NewFail("处理失败: " + e.getMessage());
}
}
```
注意:上面的`saveEntity`方法需要自己实现,它能够根据实体的类型选择正确的repository进行保存。我们可以通过一个Map来关联实体类型和对应的repository。
但是,由于我们的代码中已经有了`DynamicRepositoryService`,它可能已经包含了各个实体的repository。因此,我们可以在`AppService`中注入`DynamicRepositoryService`,然后通过它来获取repository并保存。
由于实现通用保存方法可能比较复杂,这里不展开。如果无法实现通用保存,也可以针对每个实体类型分别处理。
但是,考虑到我们已经在`AppController`中有一个`ENTITY_MAP`,我们可以类似地在`AppService`中获取实体类,然后动态调用repository。
然而,由于时间关系,我们暂时采用另一种方式:在`AppService`中为每个已知的实体类型编写处理代码。这样虽然不够动态,但更可靠。
例如:
```java
public Information saveAllData(Map<String, Object> allData) {
try {
// 处理bancais
if (allData.containsKey("bancais")) {
List<Map<String, Object>> bancais = (List<Map<String, Object>>) allData.get("bancais");
for (Map<String, Object> item : bancais) {
Bancai entity = objectMapper.convertValue(item, Bancai.class);
bancaiRepository.save(entity);
}
}
// 处理caizhis
if (allData.containsKey("caizhis")) {
// 类似处理...
}
// ... 其他实体类型
return Information.NewSuccess("数据保存成功");
} catch (Exception e) {
return Information.NewFail("处理失败: " + e.getMessage());
}
}
```
这样,虽然代码冗长,但针对每个实体类型明确处理,更不容易出错。
由于我们不知道您的所有实体类型,因此需要您根据实际情况补充。
总结步骤:
1. 修改Controller中的`saveAllData`方法,使用`String`接收请求体,然后手动解析为`Map<String, Object>`。
2. 在AppService中,根据Map的键分别处理每个实体列表,转换为实体对象并保存。
注意:如果请求体确实很大,可能需要考虑分批次保存或者使用事务来保证数据完整性。
另外,请确保在解析大JSON时内存足够。
如果问题仍然存在,请检查以下方面:
- 客户端发送的JSON是否合法?可以在Controller中打印出接收到的字符串的前100个字符,看看是否被截断。
- 是否有过滤器或拦截器修改了请求体?
最后,如果使用`String`接收请求体,请注意日志中打印的请求体可能很大,避免日志过多。
修改后的Controller方法:
```java
@PostMapping("/save-all")
public Information saveAllData(@RequestBody String allDataJson) {
try {
// 打印日志,注意:大文本不要全部打印,只打印摘要
logger.info("Received save-all request, length: {}", allDataJson.length());
// 使用Jackson解析
Map<String, Object> allData = objectMapper.readValue(allDataJson, Map.class);
return appService.saveAllData(allData);
} catch (Exception e) {
return Information.NewFail("保存全部数据失败: " + e.getMessage());
}
}
```
请尝试以上修改,并观察日志中是否打印了正确的请求体长度。
如果长度符合预期(76274),则说明请求体接收完整,问题可能出在后续的解析或处理上。
如果长度不符合,则可能是客户端未发送数据或传输问题。
希望以上方法能解决您的问题。
§§相关问题§§:
1. 在Spring Boot中,如何处理大文件上传以避免内存溢出?
2. 如何设计一个通用的JSON数据导入接口,支持多种实体类型?
3. 使用Spring Boot时,如何优化JSON序列化和反序列化的性能?
4. 在REST API中,如何实现批量数据的高效插入(例如使用JPA的批量插入)?</think>### 问题分析与解决方案
根据您提供的代码和错误信息,我发现了两个关键问题:
1. **404错误**:请求的端点未找到(但端点已定义)
2. **请求体解析失败**:日志显示`Request Parameters: {"requestBody":"{}"}`,说明请求体未被正确解析
### 根本原因分析
1. **参数类型不匹配**:
```java
@PostMapping("/save-all")
public Information saveAllData(@RequestBody Map<String, List<?>> allData) {
```
实际请求体是包含嵌套对象的复杂JSON结构(如`{bancais: [{...}], caizhis: [{...}]}`),无法直接映射到`Map<String, List<?>>`
2. **Jackson反序列化问题**:
- 嵌套对象(如`caizhi: {id: 1}`)无法直接转换为实体类
- 泛型类型擦除导致类型信息丢失
### 完整解决方案
#### 1. 修改Controller参数类型
```java
// 修改前
@PostMapping("/save-all")
public Information saveAllData(@RequestBody Map<String, List<?>> allData)
// 修改后
@PostMapping("/save-all")
public Information saveAllData(@RequestBody Map<String, Object> allData) {
try {
return appService.saveAllData(allData);
} catch (Exception e) {
return Information.NewFail("保存全部数据失败: " + e.getMessage());
}
}
```
#### 2. 增强AppService实现
```java
public Information saveAllData(Map<String, Object> allData) {
try {
// 1. 记录请求数据摘要
log.info("收到保存请求,包含 {} 种数据类型", allData.size());
// 2. 遍历所有数据类型
for (Map.Entry<String, Object> entry : allData.entrySet()) {
String entityType = entry.getKey();
Object data = entry.getValue();
// 3. 跳过非列表数据
if (!(data instanceof List)) {
log.warn("数据类型 {} 不是列表,跳过", entityType);
continue;
}
List<?> items = (List<?>) data;
log.info("处理 {}: {} 条记录", entityType, items.size());
// 4. 获取实体类信息
Class<?> entityClass = ENTITY_MAP.get(entityType.toLowerCase());
if (entityClass == null) {
log.warn("未知实体类型: {}", entityType);
continue;
}
// 5. 处理每条记录
for (Object item : items) {
if (item instanceof Map) {
// 6. 使用ObjectMapper进行深度转换
Object entity = objectMapper.convertValue(item, entityClass);
// 7. 特殊实体处理
if (entity instanceof Dingdan_bancai) {
handleDingdanBancai((Dingdan_bancai) entity, (Map<?, ?>) item);
}
// 8. 保存实体
saveEntity(entity);
}
}
}
return Information.NewSuccess("数据保存成功");
} catch (Exception e) {
log.error("保存数据失败", e);
return Information.NewFail("处理失败: " + e.getMessage());
}
}
// 特殊实体处理
private void handleDingdanBancai(Dingdan_bancai entity, Map<?, ?> item) {
if (item.containsKey("currentUserId")) {
Object userId = item.get("currentUserId");
if (userId instanceof Integer) {
entity.setCurrentUserId((Integer) userId);
}
}
}
// 通用保存方法
private void saveEntity(Object entity) {
// 根据实体类型选择对应的Repository保存
// 示例伪代码:
if (entity instanceof Bancai) {
bancaiRepository.save((Bancai) entity);
} else if (entity instanceof Caizhi) {
caizhiRepository.save((Caizhi) entity);
}
// ...其他实体类型
}
```
#### 3. 添加Jackson配置(解决嵌套对象问题)
在`application.properties`中添加:
```properties
# 启用嵌套对象支持
spring.jackson.mapper.allow-explicit-property-renaming=true
spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true
```
#### 4. 配置请求体大小限制
在`application.properties`中添加:
```properties
# 增加请求体大小限制 (默认只有2MB)
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.max-file-size=100MB
```
### 解决方案说明
1. **参数类型优化**:
- 使用`Map<String, Object>`接收动态JSON结构
- 支持嵌套对象和复杂数据类型
2. **深度对象转换**:
- 使用`objectMapper.convertValue()`处理嵌套对象
- 保留完整的类型信息
3. **特殊实体处理**:
- 为`Dingdan_bancai`等特殊实体添加额外处理逻辑
- 提取并设置`currentUserId`等特殊字段
4. **健壮的错误处理**:
- 详细的日志记录
- 跳过无效数据类型
- 异常捕获和友好错误返回
5. **性能优化**:
- 批量处理代替逐条提交
- 请求体大小限制配置
### 验证步骤
1. 使用Postman发送测试请求:
```json
POST /app/save-all
Content-Type: application/json
{
"bancais": [
{"id": 1, "caizhi": {"id": 1}, "houdu": 15},
{"id": 2, "caizhi": {"id": 2}, "houdu": 18}
],
"caizhis": [
{"id": 1, "name": "纤维板"},
{"id": 2, "name": "实木板"}
]
}
```
2. 检查日志输出:
```
收到保存请求,包含 2 种数据类型
处理 bancais: 2 条记录
处理 caizhis: 2 条记录
```
3. 验证数据库是否成功保存所有记录
阅读全文
相关推荐


















