From 1ba3fb0c85641495cd6c822a7a9e74ca57ecb668 Mon Sep 17 00:00:00 2001 From: shenquanyi Date: Thu, 4 Dec 2025 14:11:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90=E7=89=9B?= =?UTF-8?q?=E5=8F=AA=E6=95=B0=E6=8D=AE=E7=9A=84=E5=AF=BC=E5=85=A5=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CattleDataController.java | 279 ++++++++---- .../cattletends/dto/ProvinceDataDTO.java | 7 +- .../cattletends/entity/CattleBreed.java | 20 +- .../cattletends/entity/CattleProvince.java | 8 +- .../entity/ProvinceDailyPrice.java | 127 ++++++ .../repository/CattleBreedRepository.java | 8 + .../repository/CattleDataRepository.java | 79 +++- .../repository/CattleProvinceRepository.java | 12 +- .../ProvinceDailyPriceRepository.java | 54 +++ .../service/CattleBreedService.java | 6 + .../service/CattleDataService.java | 10 +- .../service/ProvinceDailyPriceService.java | 59 +++ .../service/impl/CattleBreedServiceImpl.java | 183 +++++++- .../service/impl/CattleDataServiceImpl.java | 419 ++++++++++++++++-- .../impl/CattleNationalServiceImpl.java | 17 + .../impl/CattleProvinceServiceImpl.java | 187 +++++++- .../impl/ProvinceDailyPriceServiceImpl.java | 94 ++++ .../add_indexes_for_province_daily_price.sql | 37 ++ .../migration/add_is_delet_to_cattlebreed.sql | 13 + .../db/migration/create_cattlebreed_table.sql | 8 +- .../create_province_daily_price_table.sql | 15 + ...erate_province_daily_price_last_7_days.sql | 62 +++ province_daily_price_insert.sql | 93 ++++ 23 files changed, 1651 insertions(+), 146 deletions(-) create mode 100644 backend/src/main/java/com/example/cattletends/entity/ProvinceDailyPrice.java create mode 100644 backend/src/main/java/com/example/cattletends/repository/ProvinceDailyPriceRepository.java create mode 100644 backend/src/main/java/com/example/cattletends/service/ProvinceDailyPriceService.java create mode 100644 backend/src/main/java/com/example/cattletends/service/impl/ProvinceDailyPriceServiceImpl.java create mode 100644 backend/src/main/resources/db/migration/add_indexes_for_province_daily_price.sql create mode 100644 backend/src/main/resources/db/migration/add_is_delet_to_cattlebreed.sql create mode 100644 backend/src/main/resources/db/migration/create_province_daily_price_table.sql create mode 100644 backend/src/main/resources/db/migration/generate_province_daily_price_last_7_days.sql create mode 100644 province_daily_price_insert.sql diff --git a/backend/src/main/java/com/example/cattletends/controller/CattleDataController.java b/backend/src/main/java/com/example/cattletends/controller/CattleDataController.java index 765cb30..fecac68 100644 --- a/backend/src/main/java/com/example/cattletends/controller/CattleDataController.java +++ b/backend/src/main/java/com/example/cattletends/controller/CattleDataController.java @@ -35,6 +35,7 @@ public class CattleDataController { private final com.example.cattletends.service.CattleProvinceService cattleProvinceService; private final com.example.cattletends.service.CattleNationalService cattleNationalService; private final com.example.cattletends.service.CattleBreedService cattleBreedService; + private final com.example.cattletends.service.ProvinceDailyPriceService provinceDailyPriceService; /** * 构造器注入 @@ -42,34 +43,49 @@ public class CattleDataController { public CattleDataController(CattleDataService cattleDataService, com.example.cattletends.service.CattleProvinceService cattleProvinceService, com.example.cattletends.service.CattleNationalService cattleNationalService, - com.example.cattletends.service.CattleBreedService cattleBreedService) { + com.example.cattletends.service.CattleBreedService cattleBreedService, + com.example.cattletends.service.ProvinceDailyPriceService provinceDailyPriceService) { this.cattleDataService = cattleDataService; this.cattleProvinceService = cattleProvinceService; this.cattleNationalService = cattleNationalService; this.cattleBreedService = cattleBreedService; + this.provinceDailyPriceService = provinceDailyPriceService; } /** - * 获取所有牛只数据(支持按省份或品种筛选) + * 获取所有牛只数据(支持按省份或品种筛选,支持按日期查询) */ @GetMapping - @ApiOperation(value = "获取所有牛只数据", notes = "返回所有牛只数据的列表,支持按省份或品种筛选,按价格升序排序") + @ApiOperation(value = "获取所有牛只数据", notes = "返回牛只数据的列表,支持按省份或品种筛选,支持按日期查询(格式:yyyy-MM-dd),默认查询今天的数据,按价格升序排序") public Result> getAllCattleData( @ApiParam(value = "省份(可选,如果提供则只返回该省份的数据)") @RequestParam(required = false) String province, @ApiParam(value = "品种(可选,如果提供则只返回该品种的数据)") - @RequestParam(required = false) String type) { + @RequestParam(required = false) String type, + @ApiParam(value = "日期(可选,格式:yyyy-MM-dd,如果提供则查询该日期的数据,不提供则查询今天的数据)") + @RequestParam(required = false) String date) { List data; + // 解析日期参数 + java.time.LocalDate parsedDate = null; + if (date != null && !date.trim().isEmpty()) { + try { + parsedDate = java.time.LocalDate.parse(date.trim()); + } catch (Exception e) { + System.err.println("日期格式错误: " + date + ", 错误: " + e.getMessage()); + return Result.error(400, "日期格式错误,请使用 yyyy-MM-dd 格式,例如:2025-12-04"); + } + } + // 优先按品种筛选 if (type != null && !type.trim().isEmpty()) { - data = cattleDataService.getCattleDataByType(type.trim()); + data = cattleDataService.getCattleDataByType(type.trim(), parsedDate); } else if (province != null && !province.trim().isEmpty()) { // 其次按省份筛选 - data = cattleDataService.getCattleDataByProvince(province.trim()); + data = cattleDataService.getCattleDataByProvince(province.trim(), parsedDate); } else { // 都不提供则返回所有数据 - data = cattleDataService.getAllCattleData(); + data = cattleDataService.getAllCattleData(parsedDate); } return Result.success(data); } @@ -78,21 +94,34 @@ public class CattleDataController { * 阿里云大屏接口 - 获取牛只数据(直接返回数据数组,不包含Result包装) */ @GetMapping("/screen") - @ApiOperation(value = "阿里云大屏接口", notes = "返回牛只数据数组,直接返回data数据,不包含Result包装。支持按省份或品种筛选,按价格升序排序") + @ApiOperation(value = "阿里云大屏接口", notes = "返回牛只数据数组,直接返回data数据,不包含Result包装。支持按省份或品种筛选,支持按日期查询(格式:yyyy-MM-dd),默认查询今天的数据,按价格升序排序") public List getCattleDataForScreen( @ApiParam(value = "省份(可选,如果提供则只返回该省份的数据)") @RequestParam(required = false) String province, @ApiParam(value = "品种(可选,如果提供则只返回该品种的数据)") - @RequestParam(required = false) String type) { + @RequestParam(required = false) String type, + @ApiParam(value = "日期(可选,格式:yyyy-MM-dd,如果提供则查询该日期的数据,不提供则查询今天的数据)") + @RequestParam(required = false) String date) { + // 解析日期参数 + java.time.LocalDate parsedDate = null; + if (date != null && !date.trim().isEmpty()) { + try { + parsedDate = java.time.LocalDate.parse(date.trim()); + } catch (Exception e) { + System.err.println("日期格式错误: " + date + ", 错误: " + e.getMessage()); + return new java.util.ArrayList<>(); // 返回空列表 + } + } + // 优先按品种筛选 if (type != null && !type.trim().isEmpty()) { - return cattleDataService.getCattleDataByType(type.trim()); + return cattleDataService.getCattleDataByType(type.trim(), parsedDate); } else if (province != null && !province.trim().isEmpty()) { // 其次按省份筛选 - return cattleDataService.getCattleDataByProvince(province.trim()); + return cattleDataService.getCattleDataByProvince(province.trim(), parsedDate); } else { // 都不提供则返回所有数据 - return cattleDataService.getAllCattleData(); + return cattleDataService.getAllCattleData(parsedDate); } } @@ -117,64 +146,49 @@ public class CattleDataController { public Result> importCattleData( @ApiParam(value = "Excel文件", required = true) @RequestParam("file") MultipartFile file) { - System.out.println("========== 收到导入牛只数据请求 =========="); - System.out.println("文件名: " + file.getOriginalFilename()); - System.out.println("文件大小: " + file.getSize()); - if (file.isEmpty()) { - System.out.println("错误: 文件为空"); return Result.error(400, "文件不能为空"); } try { - System.out.println("开始解析Excel文件..."); List dataList = parseExcelFile(file); - System.out.println("解析完成,共 " + (dataList != null ? dataList.size() : 0) + " 条数据"); if (dataList == null || dataList.isEmpty()) { - System.out.println("错误: Excel文件中没有有效数据"); return Result.error(400, "Excel文件中没有有效数据,请检查文件格式"); } // 自动创建品种 - System.out.println("========== 开始自动创建品种 =========="); - int newBreedCount = 0; - int existingBreedCount = 0; java.util.Set processedBreeds = new java.util.HashSet<>(); - for (CattleDataDTO dto : dataList) { if (dto.getType() != null && !dto.getType().trim().isEmpty()) { String breedName = dto.getType().trim(); - // 避免重复处理相同品种 if (!processedBreeds.contains(breedName)) { try { com.example.cattletends.entity.CattleBreed existingBreed = cattleBreedService.getBreedByName(breedName); if (existingBreed == null) { - // 新品种,创建 cattleBreedService.createOrGetBreed(breedName); - newBreedCount++; - System.out.println("✓ 创建新品种: " + breedName); - } else { - existingBreedCount++; - System.out.println("○ 品种已存在: " + breedName); } processedBreeds.add(breedName); } catch (Exception e) { - System.err.println("✗ 创建品种失败: " + breedName + ", 错误: " + e.getMessage()); + System.err.println("创建品种失败: " + breedName + ", 错误: " + e.getMessage()); } } } } - System.out.println("品种处理完成: 新增 " + newBreedCount + " 个,已存在 " + existingBreedCount + " 个,共处理 " + processedBreeds.size() + " 个品种"); - System.out.println("========== 品种创建完成 =========="); - System.out.println("开始导入数据到数据库..."); + System.out.println("========== 开始导入数据,共 " + dataList.size() + " 条 =========="); com.example.cattletends.dto.ImportResult importResult = cattleDataService.batchImportCattleData(dataList); - System.out.println("导入完成: 新增 " + importResult.getNewCount() + " 条,更新 " + importResult.getUpdateCount() + " 条"); + System.out.println("========== 导入完成: 新增 " + importResult.getNewCount() + " 条,更新 " + importResult.getUpdateCount() + " 条 =========="); - String message = String.format("导入成功:新增 %d 条,更新 %d 条,共处理 %d 条数据;自动创建 %d 个新品种,%d 个品种已存在", - importResult.getNewCount(), importResult.getUpdateCount(), importResult.getTotalCount(), - newBreedCount, existingBreedCount); + // 更新品种的逻辑删除状态 + try { + cattleBreedService.updateBreedDeleteStatus(processedBreeds); + } catch (Exception e) { + System.err.println("更新品种逻辑删除状态失败: " + e.getMessage()); + } + + String message = String.format("导入成功:新增 %d 条,更新 %d 条,共处理 %d 条数据", + importResult.getNewCount(), importResult.getUpdateCount(), importResult.getTotalCount()); return Result.success(message, importResult.getDataList()); } catch (Exception e) { System.err.println("========== 导入失败 =========="); @@ -235,6 +249,62 @@ public class CattleDataController { return Result.success(data); } + /** + * 获取省份每日均价数据(支持按省份和日期查询) + */ + @GetMapping("/province-daily-prices") + @ApiOperation(value = "获取省份每日均价数据", notes = "返回省份的每日均价记录。支持通过province参数按省份查询,支持通过priceDate参数按日期查询(格式:yyyy-MM-dd)。如果同时提供province和priceDate,则返回该省份指定日期的价格;如果只提供priceDate,则返回所有省份指定日期的价格;如果只提供province,则返回该省份所有日期的价格;如果都不提供,则返回所有省份所有日期的价格,按日期降序排序") + public Result> getProvinceDailyPrices( + @ApiParam(value = "省份名称(可选),如果提供则只返回该省份的每日均价数据", required = false) + @RequestParam(required = false) String province, + @ApiParam(value = "价格日期(可选),格式:yyyy-MM-dd,如果提供则只返回该日期的价格数据", required = false) + @RequestParam(required = false) String priceDate) { + System.out.println("========== 收到查询省份每日均价请求 =========="); + System.out.println("省份参数: " + (province != null ? province : "全部")); + System.out.println("日期参数: " + (priceDate != null ? priceDate : "全部")); + + List data; + + // 解析日期参数 + java.time.LocalDate parsedDate = null; + if (priceDate != null && !priceDate.trim().isEmpty()) { + try { + parsedDate = java.time.LocalDate.parse(priceDate.trim()); + } catch (Exception e) { + System.err.println("日期格式错误: " + priceDate + ", 错误: " + e.getMessage()); + return Result.error(400, "日期格式错误,请使用 yyyy-MM-dd 格式,例如:2025-12-03"); + } + } + + // 根据参数组合进行查询 + if (province != null && !province.trim().isEmpty() && parsedDate != null) { + // 同时提供省份和日期:查询该省份指定日期的价格 + java.util.Optional optional = + provinceDailyPriceService.getByProvinceAndPriceDate(province.trim(), parsedDate); + if (optional.isPresent()) { + data = java.util.Collections.singletonList(optional.get()); + } else { + data = new java.util.ArrayList<>(); + } + System.out.println("查询省份: " + province + ", 日期: " + parsedDate + ", 返回 " + (data != null ? data.size() : 0) + " 条记录"); + } else if (parsedDate != null) { + // 只提供日期:查询所有省份指定日期的价格 + data = provinceDailyPriceService.getByPriceDate(parsedDate); + System.out.println("查询日期: " + parsedDate + ", 返回 " + (data != null ? data.size() : 0) + " 条记录"); + } else if (province != null && !province.trim().isEmpty()) { + // 只提供省份:查询该省份所有日期的价格 + data = provinceDailyPriceService.getByProvince(province.trim()); + System.out.println("查询省份: " + province + ", 返回 " + (data != null ? data.size() : 0) + " 条记录"); + } else { + // 都不提供:查询所有省份所有日期的价格 + data = provinceDailyPriceService.getAll(); + System.out.println("查询全部省份, 返回 " + (data != null ? data.size() : 0) + " 条记录"); + } + + System.out.println("========== 查询完成 =========="); + return Result.success(data); + } + /** * 获取全国总量数据 * 自动从 cattleprovince 表计算总和 @@ -292,6 +362,11 @@ public class CattleDataController { Map result = cattleProvinceService.importProvinceData(provinceDataList); System.out.println("导入完成: " + result); + // 注意:不更新 province_daily_price 表的 price 字段 + // 该字段只通过牛只数据导入时的计算得出(计算该省份所有牛只数据的平均价格) + System.out.println("========== 省份数据导入完成 =========="); + System.out.println("注意:province_price 和 province_daily_price.price 字段只通过牛只数据导入时的计算得出"); + String message = String.format("导入成功:新增 %d 条,更新 %d 条,共处理 %d 条省份数据", result.get("createCount"), result.get("updateCount"), result.get("totalCount")); return Result.success(message, result); @@ -476,7 +551,7 @@ public class CattleDataController { } /** - * 解析价格 + * 解析价格(用于牛只数据导入,取平均值) * 支持格式: * - 数字:13.5 * - 价格范围:13.41-14.61(取平均值) @@ -535,6 +610,48 @@ public class CattleDataController { return null; } + /** + * 解析省份价格(用于省份数据导入,提取最低价格) + * 支持格式: + * - 数字:13.5 + * - 价格范围:13.4-14.9(提取最低价格 13.4) + * - 字符串数字:"13.5" + * @param priceStr 价格字符串 + * @param province 省份名称(用于错误日志) + * @return 最低价格 + */ + private BigDecimal parseProvincePrice(String priceStr, String province) { + try { + if (priceStr == null || priceStr.trim().isEmpty()) { + System.out.println("解析省份价格失败: " + province + ", 错误: 价格字符串为空"); + return null; + } + + priceStr = priceStr.trim(); + + // 处理价格范围格式,如 "13.4-14.9" + if (priceStr.contains("-")) { + String[] parts = priceStr.split("-"); + if (parts.length == 2) { + try { + // 提取最低价格(第一部分) + BigDecimal minPrice = new BigDecimal(parts[0].trim()); + return minPrice; + } catch (Exception e) { + System.out.println("解析省份价格失败: " + province + ", 价格字符串: " + priceStr + ", 错误: " + e.getMessage()); + return null; + } + } + } + + // 处理普通数字字符串 + return new BigDecimal(priceStr); + } catch (Exception e) { + System.out.println("解析省份价格失败: " + province + ", 价格字符串: " + priceStr + ", 错误: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName())); + return null; + } + } + /** * 截取所在产地到"市"级别 */ @@ -642,77 +759,85 @@ public class CattleDataController { // Excel模板列顺序(根据实际模板): // A列(索引0):省份 - // B列(索引1):2023存栏(万头) -> inventory_23th - // C列(索引2):2023出栏(万头) -> slaughter_23th - // D列(索引3):2024存栏(万头) -> inventory_24th - // E列(索引4):2024出栏(万头) -> slaughter_24th - // F列(索引5):2025存栏(万头) -> inventory_25th - // G列(索引6):2025出栏(万头) -> slaughter_25th - // H列(索引7):省份均价(元/斤) -> province_price + // B列(索引1):省份均价(元/斤) -> province_price + // C列(索引2):2023存栏(万头) -> inventory_23th + // D列(索引3):2023出栏(万头) -> slaughter_23th + // E列(索引4):2024存栏(万头) -> inventory_24th + // F列(索引5):2024出栏(万头) -> slaughter_24th + // G列(索引6):2025存栏(万头) -> inventory_25th + // H列(索引7):2025出栏(万头) -> slaughter_25th // 读取省份(A列,索引0) Cell provinceCell = row.getCell(0); if (provinceCell != null) { - String province = getCellValueAsString(provinceCell); - // 处理可能包含"省"、"自治区"等后缀的情况 - province = province.replace("省", "").replace("自治区", "").replace("市", "").trim(); + String province = getCellValueAsString(provinceCell).trim(); + // 完全按照表格内容导入,不做任何截取 dto.setProvince(province); } - // 读取2023存栏(万头)(B列,索引1)-> inventory_23th - Cell inv23Cell = row.getCell(1); - if (inv23Cell != null) { - Integer value = parseInteger(inv23Cell); - dto.setInventory23th(value); + // 读取省份均价(元/斤)(B列,索引1)-> province_price_range(原始区间)和 province_price(最低价格) + Cell priceCell = row.getCell(1); + if (priceCell != null) { + String priceStr = getCellValueAsString(priceCell); + System.out.println("省份: " + dto.getProvince() + ", 原始价格字符串: [" + priceStr + "]"); + + // 保存原始价格区间字符串 + dto.setProvincePriceRange(priceStr); + + // 解析价格:如果是区间,提取最低价格;否则使用原值 + BigDecimal price = parseProvincePrice(priceStr, dto.getProvince() != null ? dto.getProvince() : "未知省份"); + dto.setProvincePrice(price); + System.out.println("省份: " + dto.getProvince() + ", 价格区间: " + priceStr + ", 最低价格: " + price); + } else { + System.out.println("省份: " + dto.getProvince() + ", 价格单元格为空或不存在"); } - // 读取2023出栏(万头)(C列,索引2)-> slaughter_23th - Cell sla23Cell = row.getCell(2); + // 读取2023存栏(万头)(C列,索引2)-> inventory_23th + Cell inv23Cell = row.getCell(2); + if (inv23Cell != null) { + String cellValueStr = getCellValueAsString(inv23Cell); + Integer value = parseInteger(inv23Cell); + System.out.println("省份: " + dto.getProvince() + ", 2023存栏原始值: [" + cellValueStr + "], 解析后: " + value); + dto.setInventory23th(value); + } else { + System.out.println("省份: " + dto.getProvince() + ", 2023存栏单元格为空"); + } + + // 读取2023出栏(万头)(D列,索引3)-> slaughter_23th + Cell sla23Cell = row.getCell(3); if (sla23Cell != null) { Integer value = parseInteger(sla23Cell); dto.setSlaughter23th(value); } - // 读取2024存栏(万头)(D列,索引3)-> inventory_24th - Cell inv24Cell = row.getCell(3); + // 读取2024存栏(万头)(E列,索引4)-> inventory_24th + Cell inv24Cell = row.getCell(4); if (inv24Cell != null) { Integer value = parseInteger(inv24Cell); dto.setInventory24th(value); } - // 读取2024出栏(万头)(E列,索引4)-> slaughter_24th - Cell sla24Cell = row.getCell(4); + // 读取2024出栏(万头)(F列,索引5)-> slaughter_24th + Cell sla24Cell = row.getCell(5); if (sla24Cell != null) { Integer value = parseInteger(sla24Cell); dto.setSlaughter24th(value); } - // 读取2025存栏(万头)(F列,索引5)-> inventory_25th - Cell inv25Cell = row.getCell(5); + // 读取2025存栏(万头)(G列,索引6)-> inventory_25th + Cell inv25Cell = row.getCell(6); if (inv25Cell != null) { Integer value = parseInteger(inv25Cell); dto.setInventory25th(value); } - // 读取2025出栏(万头)(G列,索引6)-> slaughter_25th - Cell sla25Cell = row.getCell(6); + // 读取2025出栏(万头)(H列,索引7)-> slaughter_25th + Cell sla25Cell = row.getCell(7); if (sla25Cell != null) { Integer value = parseInteger(sla25Cell); dto.setSlaughter25th(value); } - // 读取省份均价(元/斤)(H列,索引7)-> province_price - Cell priceCell = row.getCell(7); - if (priceCell != null) { - String priceStr = getCellValueAsString(priceCell); - System.out.println("省份: " + dto.getProvince() + ", 原始价格字符串: [" + priceStr + "]"); - BigDecimal price = parsePrice(priceCell, dto.getProvince() != null ? dto.getProvince() : "未知省份"); - dto.setProvincePrice(price); - System.out.println("省份: " + dto.getProvince() + ", 解析后价格: " + price); - } else { - System.out.println("省份: " + dto.getProvince() + ", 价格单元格为空或不存在"); - } - // 验证必填字段 if (dto.getProvince() != null && !dto.getProvince().trim().isEmpty()) { dataList.add(dto); diff --git a/backend/src/main/java/com/example/cattletends/dto/ProvinceDataDTO.java b/backend/src/main/java/com/example/cattletends/dto/ProvinceDataDTO.java index f962f13..7ea238c 100644 --- a/backend/src/main/java/com/example/cattletends/dto/ProvinceDataDTO.java +++ b/backend/src/main/java/com/example/cattletends/dto/ProvinceDataDTO.java @@ -20,9 +20,14 @@ public class ProvinceDataDTO { private String province; /** - * 省份均价 + * 省份均价(最低价格) */ private BigDecimal provincePrice; + + /** + * 省份均价区间(原始字符串,如 "13.4-14.9") + */ + private String provincePriceRange; /** * 23年存栏(万头) diff --git a/backend/src/main/java/com/example/cattletends/entity/CattleBreed.java b/backend/src/main/java/com/example/cattletends/entity/CattleBreed.java index b607201..279a6eb 100644 --- a/backend/src/main/java/com/example/cattletends/entity/CattleBreed.java +++ b/backend/src/main/java/com/example/cattletends/entity/CattleBreed.java @@ -10,7 +10,8 @@ import java.time.LocalDateTime; @Table(name = "cattlebreed", indexes = { @Index(name = "idx_breed_name", columnList = "breed_name", unique = true), @Index(name = "idx_create_time", columnList = "create_time"), - @Index(name = "idx_up_time", columnList = "up_time") + @Index(name = "idx_up_time", columnList = "up_time"), + @Index(name = "idx_is_delet", columnList = "is_delet") }) public class CattleBreed { @@ -36,10 +37,19 @@ public class CattleBreed { @Column(name = "up_time", nullable = false) private LocalDateTime upTime; + /** + * 逻辑删除标志:0=未删除,1=已删除 + */ + @Column(name = "is_delet", nullable = false, columnDefinition = "INT DEFAULT 0") + private Integer isDelet = 0; + @PrePersist protected void onCreate() { createTime = LocalDateTime.now(); upTime = LocalDateTime.now(); + if (isDelet == null) { + isDelet = 0; + } } @PreUpdate @@ -79,5 +89,13 @@ public class CattleBreed { public void setUpTime(LocalDateTime upTime) { this.upTime = upTime; } + + public Integer getIsDelet() { + return isDelet; + } + + public void setIsDelet(Integer isDelet) { + this.isDelet = isDelet; + } } diff --git a/backend/src/main/java/com/example/cattletends/entity/CattleProvince.java b/backend/src/main/java/com/example/cattletends/entity/CattleProvince.java index b7c57a0..6832584 100644 --- a/backend/src/main/java/com/example/cattletends/entity/CattleProvince.java +++ b/backend/src/main/java/com/example/cattletends/entity/CattleProvince.java @@ -42,10 +42,16 @@ public class CattleProvince { private String province; /** - * 省份均价 + * 省份均价(存储最低价格) */ @Column(name = "province_price", precision = 10, scale = 2) private BigDecimal provincePrice; + + /** + * 省份均价区间(原始字符串,如 "13.4-14.9") + */ + @Column(name = "province_price_range", length = 50) + private String provincePriceRange; /** * 23年存栏(万头) diff --git a/backend/src/main/java/com/example/cattletends/entity/ProvinceDailyPrice.java b/backend/src/main/java/com/example/cattletends/entity/ProvinceDailyPrice.java new file mode 100644 index 0000000..1e6683f --- /dev/null +++ b/backend/src/main/java/com/example/cattletends/entity/ProvinceDailyPrice.java @@ -0,0 +1,127 @@ +package com.example.cattletends.entity; + +import javax.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 省份每日均价实体类 + * 存储15个省份每天的省份均价,只累加不覆盖 + */ +@Entity +@Table(name = "province_daily_price", + uniqueConstraints = { + @UniqueConstraint(name = "uk_province_date", columnNames = {"province", "price_date"}) + }, + indexes = { + @Index(name = "idx_province", columnList = "province"), + @Index(name = "idx_price_date", columnList = "price_date"), + @Index(name = "idx_province_date_composite", columnList = "province,price_date"), + @Index(name = "idx_price", columnList = "price"), + @Index(name = "idx_date_price", columnList = "price_date,price"), + @Index(name = "idx_province_price", columnList = "province,price"), + @Index(name = "idx_up_time", columnList = "up_time"), + @Index(name = "idx_create_time", columnList = "create_time") + } +) +public class ProvinceDailyPrice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 省份名称 + */ + @Column(name = "province", nullable = false, length = 100) + private String province; + + /** + * 省份均价(元/斤) + */ + @Column(name = "price", precision = 10, scale = 2, nullable = false) + private BigDecimal price; + + /** + * 价格日期(只记录日期,不包含时间) + */ + @Column(name = "price_date", nullable = false) + private LocalDate priceDate; + + /** + * 创建时间 + */ + @Column(name = "create_time") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Column(name = "up_time", nullable = false) + private LocalDateTime upTime; + + @PrePersist + protected void onCreate() { + createTime = LocalDateTime.now(); + upTime = LocalDateTime.now(); + if (priceDate == null) { + priceDate = LocalDate.now(); + } + } + + @PreUpdate + protected void onUpdate() { + upTime = LocalDateTime.now(); + } + + // Getters and Setters + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public LocalDate getPriceDate() { + return priceDate; + } + + public void setPriceDate(LocalDate priceDate) { + this.priceDate = priceDate; + } + + public LocalDateTime getCreateTime() { + return createTime; + } + + public void setCreateTime(LocalDateTime createTime) { + this.createTime = createTime; + } + + public LocalDateTime getUpTime() { + return upTime; + } + + public void setUpTime(LocalDateTime upTime) { + this.upTime = upTime; + } +} + diff --git a/backend/src/main/java/com/example/cattletends/repository/CattleBreedRepository.java b/backend/src/main/java/com/example/cattletends/repository/CattleBreedRepository.java index 39f90e2..d9f1844 100644 --- a/backend/src/main/java/com/example/cattletends/repository/CattleBreedRepository.java +++ b/backend/src/main/java/com/example/cattletends/repository/CattleBreedRepository.java @@ -2,8 +2,10 @@ package com.example.cattletends.repository; import com.example.cattletends.entity.CattleBreed; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; /** @@ -21,5 +23,11 @@ public interface CattleBreedRepository extends JpaRepository findAllNotDeleted(); } diff --git a/backend/src/main/java/com/example/cattletends/repository/CattleDataRepository.java b/backend/src/main/java/com/example/cattletends/repository/CattleDataRepository.java index 768750c..2f70c41 100644 --- a/backend/src/main/java/com/example/cattletends/repository/CattleDataRepository.java +++ b/backend/src/main/java/com/example/cattletends/repository/CattleDataRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -35,14 +37,87 @@ public interface CattleDataRepository extends JpaRepository /** * 根据品种、省份、产地、价格查询牛只数据(用于判断重复) + * 注意:使用 findFirstBy,避免因为存在多条相同记录导致 NonUniqueResultException * * @param type 品种 * @param province 省份 * @param location 产地 * @param price 价格 - * @return 牛只数据(如果存在) + * @return 第一条匹配的牛只数据(如果存在) */ - Optional findByTypeAndProvinceAndLocationAndPrice( + Optional findFirstByTypeAndProvinceAndLocationAndPrice( String type, String province, String location, BigDecimal price); + + /** + * 根据品种和产地查询牛只数据(用于判断重复) + * 注意:使用 findFirstBy,避免因为存在多条相同记录导致 NonUniqueResultException + * + * @param type 品种 + * @param location 产地 + * @return 第一条匹配的牛只数据(如果存在) + */ + Optional findFirstByTypeAndLocation(String type, String location); + + /** + * 根据品种和产地查询所有匹配的牛只数据(用于删除重复记录) + * 使用 TRIM 函数来匹配,忽略前后空格 + * + * @param type 品种 + * @param location 产地 + * @return 所有匹配的牛只数据列表 + */ + @org.springframework.data.jpa.repository.Query("SELECT c FROM CattleData c WHERE TRIM(c.type) = TRIM(?1) AND TRIM(c.location) = TRIM(?2)") + List findByTypeAndLocation(String type, String location); + + /** + * 获取所有不重复的省份列表 + * + * @return 省份列表 + */ + @org.springframework.data.jpa.repository.Query("SELECT DISTINCT c.province FROM CattleData c WHERE c.province IS NOT NULL AND c.province != ''") + List findAllDistinctProvinces(); + + /** + * 根据日期范围查询牛只数据(用于删除当天数据) + * + * @param startTime 开始时间(当天00:00:00) + * @param endTime 结束时间(当天23:59:59) + * @return 该日期范围内的所有数据 + */ + @org.springframework.data.jpa.repository.Query("SELECT c FROM CattleData c WHERE c.createTime >= ?1 AND c.createTime <= ?2") + List findByCreateTimeBetween(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据日期查询当天的牛只数据(用于查询接口) + * 使用DATE()函数比较日期部分,忽略时间部分 + * + * @param date 日期(只比较日期部分,忽略时间) + * @param sort 排序规则 + * @return 当天的数据列表 + */ + @org.springframework.data.jpa.repository.Query("SELECT c FROM CattleData c WHERE DATE(c.createTime) = DATE(?1)") + List findByCreateTimeDate(LocalDate date, Sort sort); + + /** + * 根据省份和日期查询当天的牛只数据 + * + * @param province 省份 + * @param date 日期 + * @param sort 排序规则 + * @return 该省份当天的数据列表 + */ + @org.springframework.data.jpa.repository.Query("SELECT c FROM CattleData c WHERE c.province = ?1 AND DATE(c.createTime) = DATE(?2)") + List findByProvinceAndCreateTimeDate(String province, LocalDate date, Sort sort); + + /** + * 根据品种和日期查询当天的牛只数据 + * + * @param type 品种 + * @param date 日期 + * @param sort 排序规则 + * @return 该品种当天的数据列表 + */ + @org.springframework.data.jpa.repository.Query("SELECT c FROM CattleData c WHERE c.type = ?1 AND DATE(c.createTime) = DATE(?2)") + List findByTypeAndCreateTimeDate(String type, LocalDate date, Sort sort); } diff --git a/backend/src/main/java/com/example/cattletends/repository/CattleProvinceRepository.java b/backend/src/main/java/com/example/cattletends/repository/CattleProvinceRepository.java index 1f1efa2..5e43d46 100644 --- a/backend/src/main/java/com/example/cattletends/repository/CattleProvinceRepository.java +++ b/backend/src/main/java/com/example/cattletends/repository/CattleProvinceRepository.java @@ -15,12 +15,12 @@ import java.util.Optional; public interface CattleProvinceRepository extends JpaRepository { /** - * 根据省份名称查询 + * 根据省份名称查询(返回第一条记录) * * @param province 省份名称 * @return 省份数据 */ - Optional findByProvince(String province); + Optional findFirstByProvince(String province); /** * 根据省份名称查询(支持排序) @@ -30,5 +30,13 @@ public interface CattleProvinceRepository extends JpaRepository findByProvince(String province, Sort sort); + + /** + * 根据省份名称查询所有记录(用于去重处理) + * + * @param province 省份名称 + * @return 省份数据列表 + */ + List findAllByProvince(String province); } diff --git a/backend/src/main/java/com/example/cattletends/repository/ProvinceDailyPriceRepository.java b/backend/src/main/java/com/example/cattletends/repository/ProvinceDailyPriceRepository.java new file mode 100644 index 0000000..b9ff819 --- /dev/null +++ b/backend/src/main/java/com/example/cattletends/repository/ProvinceDailyPriceRepository.java @@ -0,0 +1,54 @@ +package com.example.cattletends.repository; + +import com.example.cattletends.entity.ProvinceDailyPrice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +/** + * 省份每日均价Repository + */ +@Repository +public interface ProvinceDailyPriceRepository extends JpaRepository { + + /** + * 根据省份和日期查询(用于检查是否已存在,避免重复) + */ + Optional findByProvinceAndPriceDate(String province, LocalDate priceDate); + + /** + * 检查省份和日期是否存在 + */ + boolean existsByProvinceAndPriceDate(String province, LocalDate priceDate); + + /** + * 根据省份查询所有记录,按日期降序排序 + */ + List findByProvinceOrderByPriceDateDesc(String province); + + /** + * 查询所有记录,按日期降序排序 + */ + List findAllByOrderByPriceDateDesc(); + + /** + * 根据省份和日期范围查询 + */ + @Query("SELECT p FROM ProvinceDailyPrice p WHERE p.province = :province AND p.priceDate BETWEEN :startDate AND :endDate ORDER BY p.priceDate DESC") + List findByProvinceAndDateRange( + @Param("province") String province, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + + /** + * 根据日期查询所有省份的每日均价记录,按省份名称升序排序 + */ + List findByPriceDateOrderByProvinceAsc(LocalDate priceDate); +} + diff --git a/backend/src/main/java/com/example/cattletends/service/CattleBreedService.java b/backend/src/main/java/com/example/cattletends/service/CattleBreedService.java index cc0e30f..989c98b 100644 --- a/backend/src/main/java/com/example/cattletends/service/CattleBreedService.java +++ b/backend/src/main/java/com/example/cattletends/service/CattleBreedService.java @@ -28,5 +28,11 @@ public interface CattleBreedService { * @return 品种实体 */ CattleBreed getBreedByName(String breedName); + + /** + * 更新品种的逻辑删除状态 + * @param breedNamesInExcel Excel中存在的品种名称集合 + */ + void updateBreedDeleteStatus(java.util.Set breedNamesInExcel); } diff --git a/backend/src/main/java/com/example/cattletends/service/CattleDataService.java b/backend/src/main/java/com/example/cattletends/service/CattleDataService.java index 597ce76..5dd26ca 100644 --- a/backend/src/main/java/com/example/cattletends/service/CattleDataService.java +++ b/backend/src/main/java/com/example/cattletends/service/CattleDataService.java @@ -4,6 +4,7 @@ import com.example.cattletends.dto.CattleDataDTO; import com.example.cattletends.dto.ImportResult; import com.example.cattletends.entity.CattleData; +import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -15,25 +16,28 @@ public interface CattleDataService { /** * 获取所有牛只数据 * + * @param date 日期(可选,如果为null则查询今天的数据) * @return 牛只数据列表 */ - List getAllCattleData(); + List getAllCattleData(LocalDate date); /** * 根据省份获取牛只数据 * * @param province 省份 + * @param date 日期(可选,如果为null则查询今天的数据) * @return 牛只数据列表 */ - List getCattleDataByProvince(String province); + List getCattleDataByProvince(String province, LocalDate date); /** * 根据品种获取牛只数据 * * @param type 品种 + * @param date 日期(可选,如果为null则查询今天的数据) * @return 牛只数据列表 */ - List getCattleDataByType(String type); + List getCattleDataByType(String type, LocalDate date); /** * 根据ID获取牛只数据 diff --git a/backend/src/main/java/com/example/cattletends/service/ProvinceDailyPriceService.java b/backend/src/main/java/com/example/cattletends/service/ProvinceDailyPriceService.java new file mode 100644 index 0000000..ae1093d --- /dev/null +++ b/backend/src/main/java/com/example/cattletends/service/ProvinceDailyPriceService.java @@ -0,0 +1,59 @@ +package com.example.cattletends.service; + +import com.example.cattletends.entity.ProvinceDailyPrice; + +import java.time.LocalDate; +import java.util.List; + +/** + * 省份每日均价服务接口 + */ +public interface ProvinceDailyPriceService { + + /** + * 保存或更新省份每日均价(如果当天该省份已有数据,则更新价格) + * @param province 省份名称 + * @param price 省份均价 + * @param priceDate 价格日期(如果为null,则使用当前日期) + * @return 保存或更新的实体,isNew=true表示新建,isNew=false表示更新 + */ + ProvinceDailyPrice saveOrUpdate(String province, java.math.BigDecimal price, LocalDate priceDate); + + /** + * 根据省份查询所有每日均价记录,按日期降序排序 + * @param province 省份名称 + * @return 省份每日均价列表 + */ + List getByProvince(String province); + + /** + * 获取所有省份每日均价记录,按日期降序排序 + * @return 所有省份每日均价列表 + */ + List getAll(); + + /** + * 根据省份和日期范围查询 + * @param province 省份名称 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 省份每日均价列表 + */ + List getByProvinceAndDateRange(String province, LocalDate startDate, LocalDate endDate); + + /** + * 根据日期查询所有省份的每日均价记录 + * @param priceDate 价格日期 + * @return 省份每日均价列表 + */ + List getByPriceDate(LocalDate priceDate); + + /** + * 根据省份和日期查询 + * @param province 省份名称 + * @param priceDate 价格日期 + * @return 省份每日均价(如果存在) + */ + java.util.Optional getByProvinceAndPriceDate(String province, LocalDate priceDate); +} + diff --git a/backend/src/main/java/com/example/cattletends/service/impl/CattleBreedServiceImpl.java b/backend/src/main/java/com/example/cattletends/service/impl/CattleBreedServiceImpl.java index e289945..391d6c4 100644 --- a/backend/src/main/java/com/example/cattletends/service/impl/CattleBreedServiceImpl.java +++ b/backend/src/main/java/com/example/cattletends/service/impl/CattleBreedServiceImpl.java @@ -31,21 +31,139 @@ public class CattleBreedServiceImpl implements CattleBreedService { // 先查询是否已存在 Optional existing = cattleBreedRepository.findByBreedName(trimmedName); if (existing.isPresent()) { - System.out.println("品种已存在: " + trimmedName); - return existing.get(); + CattleBreed breed = existing.get(); + // 如果品种被标记为删除,恢复它(设置为未删除) + if (breed.getIsDelet() != null && breed.getIsDelet() == 1) { + breed.setIsDelet(0); + breed = cattleBreedRepository.save(breed); + System.out.println("恢复已删除的品种: " + trimmedName); + } else { + System.out.println("品种已存在: " + trimmedName); + } + return breed; } // 不存在则创建 CattleBreed breed = new CattleBreed(); breed.setBreedName(trimmedName); + breed.setIsDelet(0); // 新创建的品种默认为未删除 CattleBreed saved = cattleBreedRepository.save(breed); System.out.println("创建新品种: " + trimmedName + " (ID: " + saved.getId() + ")"); return saved; } @Override + @Transactional public List getAllBreeds() { - return cattleBreedRepository.findAll(); + // 只查询未删除的品种(is_delet=0) + List dataList = cattleBreedRepository.findAllNotDeleted(); + + // 去重处理:如果同一个品种名称有多条记录,保留最新的一条(up_time最大),更新其他记录 + if (dataList != null && !dataList.isEmpty()) { + System.out.println("========== 开始品种去重处理 =========="); + System.out.println("去重前记录数: " + dataList.size()); + + // 按品种名称分组,找出每个品种的最新记录 + java.util.Map breedMap = new java.util.HashMap<>(); + java.util.List duplicatesToUpdate = new java.util.ArrayList<>(); + + for (CattleBreed item : dataList) { + String breedName = item.getBreedName(); + if (breedName == null || breedName.trim().isEmpty()) { + continue; + } + + String trimmedName = breedName.trim(); + + if (!breedMap.containsKey(trimmedName)) { + // 第一次遇到该品种,直接添加 + breedMap.put(trimmedName, item); + } else { + // 已存在该品种,比较更新时间,保留最新的 + CattleBreed existing = breedMap.get(trimmedName); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + // 当前记录更新,标记旧记录需要更新 + duplicatesToUpdate.add(existing); + breedMap.put(trimmedName, item); + } else { + // 现有记录更新,标记当前记录需要更新 + duplicatesToUpdate.add(item); + } + } else if (item.getUpTime() != null) { + // 当前记录有更新时间,保留它 + duplicatesToUpdate.add(existing); + breedMap.put(trimmedName, item); + } else if (existing.getUpTime() != null) { + // 现有记录有更新时间,保留现有记录 + duplicatesToUpdate.add(item); + } else { + // 都没有更新时间,保留ID较小的(通常是最早创建的) + if (item.getId() < existing.getId()) { + duplicatesToUpdate.add(existing); + breedMap.put(trimmedName, item); + } else { + duplicatesToUpdate.add(item); + } + } + } + } + + // 更新重复记录:将旧记录的数据更新为最新记录的数据(实际上品种只有名称,所以只需要更新 up_time) + int updatedCount = 0; + for (CattleBreed duplicate : duplicatesToUpdate) { + CattleBreed latest = breedMap.get(duplicate.getBreedName().trim()); + if (latest != null && !latest.getId().equals(duplicate.getId())) { + // 更新旧记录的 up_time 为最新记录的 up_time + duplicate.setUpTime(latest.getUpTime()); + cattleBreedRepository.save(duplicate); + updatedCount++; + } + } + + // 如果更新了重复记录,重新查询 + if (updatedCount > 0) { + System.out.println("更新了 " + updatedCount + " 条重复品种记录"); + dataList = cattleBreedRepository.findAll(); + } + + // 最终去重:按品种名称去重,每个品种只保留一条(保留最新的) + java.util.Map finalMap = new java.util.LinkedHashMap<>(); + for (CattleBreed item : dataList) { + String breedName = item.getBreedName(); + if (breedName == null || breedName.trim().isEmpty()) { + continue; + } + + String trimmedName = breedName.trim(); + + if (!finalMap.containsKey(trimmedName)) { + finalMap.put(trimmedName, item); + } else { + CattleBreed existing = finalMap.get(trimmedName); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + finalMap.put(trimmedName, item); + } + } else if (item.getUpTime() != null) { + finalMap.put(trimmedName, item); + } else if (existing.getUpTime() != null) { + // 保持现有记录 + } else { + // 都没有更新时间,保留ID较小的 + if (item.getId() < existing.getId()) { + finalMap.put(trimmedName, item); + } + } + } + } + + dataList = new java.util.ArrayList<>(finalMap.values()); + System.out.println("去重后记录数: " + dataList.size()); + System.out.println("========== 品种去重处理完成 =========="); + } + + return dataList; } @Override @@ -56,5 +174,64 @@ public class CattleBreedServiceImpl implements CattleBreedService { return cattleBreedRepository.findByBreedName(breedName.trim()) .orElse(null); } + + @Override + @Transactional + public void updateBreedDeleteStatus(java.util.Set breedNamesInExcel) { + System.out.println("========== 开始更新品种逻辑删除状态 =========="); + + // 获取所有品种(包括已删除的) + List allBreeds = cattleBreedRepository.findAll(); + System.out.println("数据库中总品种数: " + (allBreeds != null ? allBreeds.size() : 0)); + System.out.println("Excel中的品种: " + (breedNamesInExcel != null ? breedNamesInExcel.size() : 0) + " 个"); + + if (allBreeds == null || allBreeds.isEmpty()) { + System.out.println("数据库中没有品种数据,跳过逻辑删除状态更新"); + return; + } + + int restoredCount = 0; // 恢复的品种数(从已删除改为未删除) + int deletedCount = 0; // 标记为删除的品种数 + + // 遍历所有品种,更新逻辑删除状态 + for (CattleBreed breed : allBreeds) { + if (breed == null || breed.getBreedName() == null) { + continue; + } + + String breedName = breed.getBreedName().trim(); + boolean existsInExcel = breedNamesInExcel != null && breedNamesInExcel.contains(breedName); + + Integer currentIsDelet = breed.getIsDelet(); + if (currentIsDelet == null) { + currentIsDelet = 0; + } + + if (existsInExcel) { + // Excel中存在该品种,确保is_delet=0(未删除) + if (currentIsDelet != 0) { + breed.setIsDelet(0); + cattleBreedRepository.save(breed); + restoredCount++; + System.out.println("✓ 恢复品种: " + breedName + " (从已删除改为未删除)"); + } else { + System.out.println("○ 品种已存在且未删除: " + breedName); + } + } else { + // Excel中不存在该品种,设置is_delet=1(已删除) + if (currentIsDelet != 1) { + breed.setIsDelet(1); + cattleBreedRepository.save(breed); + deletedCount++; + System.out.println("✗ 标记品种为已删除: " + breedName); + } else { + System.out.println("○ 品种已标记为删除: " + breedName); + } + } + } + + System.out.println("品种逻辑删除状态更新完成: 恢复 " + restoredCount + " 个,标记删除 " + deletedCount + " 个"); + System.out.println("========== 品种逻辑删除状态更新完成 =========="); + } } diff --git a/backend/src/main/java/com/example/cattletends/service/impl/CattleDataServiceImpl.java b/backend/src/main/java/com/example/cattletends/service/impl/CattleDataServiceImpl.java index 60c1683..3c3c180 100644 --- a/backend/src/main/java/com/example/cattletends/service/impl/CattleDataServiceImpl.java +++ b/backend/src/main/java/com/example/cattletends/service/impl/CattleDataServiceImpl.java @@ -5,10 +5,12 @@ import com.example.cattletends.dto.ImportResult; import com.example.cattletends.entity.CattleData; import com.example.cattletends.repository.CattleDataRepository; import com.example.cattletends.service.CattleDataService; +import com.example.cattletends.service.ProvinceDailyPriceService; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; @@ -22,33 +24,193 @@ import java.util.Map; public class CattleDataServiceImpl implements CattleDataService { private final CattleDataRepository cattleDataRepository; + private final ProvinceDailyPriceService provinceDailyPriceService; + private final com.example.cattletends.service.CattleProvinceService cattleProvinceService; + private final com.example.cattletends.repository.CattleProvinceRepository cattleProvinceRepository; /** * 构造器注入 */ - public CattleDataServiceImpl(CattleDataRepository cattleDataRepository) { + public CattleDataServiceImpl(CattleDataRepository cattleDataRepository, + ProvinceDailyPriceService provinceDailyPriceService, + com.example.cattletends.service.CattleProvinceService cattleProvinceService, + com.example.cattletends.repository.CattleProvinceRepository cattleProvinceRepository) { this.cattleDataRepository = cattleDataRepository; + this.provinceDailyPriceService = provinceDailyPriceService; + this.cattleProvinceService = cattleProvinceService; + this.cattleProvinceRepository = cattleProvinceRepository; } @Override - @Transactional(readOnly = true) - public List getAllCattleData() { - // 按价格升序排序 - return cattleDataRepository.findAll(Sort.by(Sort.Direction.ASC, "price")); + @Transactional + public List getAllCattleData(LocalDate date) { + // 如果date为null,使用今天 + LocalDate queryDate = date != null ? date : LocalDate.now(); + Sort sort = Sort.by(Sort.Direction.ASC, "price"); + + // 查询指定日期的数据 + List dataList = cattleDataRepository.findByCreateTimeDate(queryDate, sort); + return dataList; } @Override - @Transactional(readOnly = true) - public List getCattleDataByProvince(String province) { - // 按价格升序排序 - return cattleDataRepository.findByProvince(province, Sort.by(Sort.Direction.ASC, "price")); + @Transactional + public List getCattleDataByProvince(String province, LocalDate date) { + // 如果date为null,使用今天 + LocalDate queryDate = date != null ? date : LocalDate.now(); + Sort sort = Sort.by(Sort.Direction.ASC, "price"); + + // 查询指定日期和省份的数据 + List dataList = cattleDataRepository.findByProvinceAndCreateTimeDate(province.trim(), queryDate, sort); + return dataList; } @Override - @Transactional(readOnly = true) - public List getCattleDataByType(String type) { - // 按价格升序排序 - return cattleDataRepository.findByType(type, Sort.by(Sort.Direction.ASC, "price")); + @Transactional + public List getCattleDataByType(String type, LocalDate date) { + // 如果date为null,使用今天 + LocalDate queryDate = date != null ? date : LocalDate.now(); + Sort sort = Sort.by(Sort.Direction.ASC, "price"); + + // 查询指定日期和品种的数据 + List dataList = cattleDataRepository.findByTypeAndCreateTimeDate(type.trim(), queryDate, sort); + return dataList; + } + + /** + * 按 location 字段去重处理 + * 如果同一个 location 有多条记录,保留最新的一条(up_time最大),更新其他记录 + * @param dataList 原始数据列表 + * @param province 省份筛选条件(可选,用于重新查询时保持筛选) + * @param type 品种筛选条件(可选,用于重新查询时保持筛选) + */ + private List deduplicateByLocation(List dataList, String province, String type) { + if (dataList == null || dataList.isEmpty()) { + return dataList; + } + + System.out.println("[查询去重] 开始按 type + location 去重处理"); + System.out.println("[查询去重] 去重前记录数: " + dataList.size()); + + // 按 type + location 分组,找出每个 type + location 组合的最新记录 + java.util.Map typeLocationMap = new java.util.HashMap<>(); + java.util.List duplicatesToUpdate = new java.util.ArrayList<>(); + + for (CattleData item : dataList) { + String itemType = item.getType(); + String location = item.getLocation(); + if ((itemType == null || itemType.trim().isEmpty()) || + (location == null || location.trim().isEmpty())) { + continue; + } + + String trimmedType = itemType.trim(); + String trimmedLocation = location.trim(); + String key = trimmedType + "|" + trimmedLocation; // 使用 | 作为分隔符 + + if (!typeLocationMap.containsKey(key)) { + // 第一次遇到该 type + location 组合,直接添加 + typeLocationMap.put(key, item); + } else { + // 已存在该 type + location 组合,比较更新时间,保留最新的 + CattleData existing = typeLocationMap.get(key); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + // 当前记录更新,标记旧记录需要更新 + duplicatesToUpdate.add(existing); + typeLocationMap.put(key, item); + } else { + // 现有记录更新,标记当前记录需要更新 + duplicatesToUpdate.add(item); + } + } else if (item.getUpTime() != null) { + // 当前记录有更新时间,保留它 + duplicatesToUpdate.add(existing); + typeLocationMap.put(key, item); + } else if (existing.getUpTime() != null) { + // 现有记录有更新时间,保留现有记录 + duplicatesToUpdate.add(item); + } else { + // 都没有更新时间,保留ID较小的(通常是最早创建的) + if (item.getId() < existing.getId()) { + duplicatesToUpdate.add(existing); + typeLocationMap.put(key, item); + } else { + duplicatesToUpdate.add(item); + } + } + } + } + + // 删除重复记录:只保留最新的记录,删除其他所有重复记录 + int deletedCount = 0; + for (CattleData duplicate : duplicatesToUpdate) { + String duplicateType = duplicate.getType() != null ? duplicate.getType().trim() : ""; + String duplicateLocation = duplicate.getLocation() != null ? duplicate.getLocation().trim() : ""; + String key = duplicateType + "|" + duplicateLocation; + CattleData latest = typeLocationMap.get(key); + if (latest != null && !latest.getId().equals(duplicate.getId())) { + // 删除重复记录,只保留最新的 + System.out.println("[查询去重] 删除重复记录: ID=" + duplicate.getId() + ", type=[" + duplicateType + "], location=[" + duplicateLocation + "]"); + cattleDataRepository.delete(duplicate); + deletedCount++; + } + } + + // 如果删除了重复记录,重新查询(保持原有的筛选条件) + if (deletedCount > 0) { + System.out.println("[查询去重] 删除了 " + deletedCount + " 条重复记录"); + // 重新查询数据(保持原有排序和筛选条件) + Sort sort = Sort.by(Sort.Direction.ASC, "price"); + if (type != null && !type.trim().isEmpty()) { + dataList = cattleDataRepository.findByType(type.trim(), sort); + } else if (province != null && !province.trim().isEmpty()) { + dataList = cattleDataRepository.findByProvince(province.trim(), sort); + } else { + dataList = cattleDataRepository.findAll(sort); + } + } + + // 最终去重:按 type + location 去重,每个 type + location 组合只保留一条(保留最新的) + java.util.Map finalMap = new java.util.LinkedHashMap<>(); + for (CattleData item : dataList) { + String itemType = item.getType(); + String location = item.getLocation(); + if ((itemType == null || itemType.trim().isEmpty()) || + (location == null || location.trim().isEmpty())) { + continue; + } + + String trimmedType = itemType.trim(); + String trimmedLocation = location.trim(); + String key = trimmedType + "|" + trimmedLocation; + + if (!finalMap.containsKey(key)) { + finalMap.put(key, item); + } else { + CattleData existing = finalMap.get(key); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + finalMap.put(key, item); + } + } else if (item.getUpTime() != null) { + finalMap.put(key, item); + } else if (existing.getUpTime() != null) { + // 保持现有记录 + } else { + // 都没有更新时间,保留ID较小的 + if (item.getId() < existing.getId()) { + finalMap.put(key, item); + } + } + } + } + + dataList = new java.util.ArrayList<>(finalMap.values()); + System.out.println("[查询去重] 去重后记录数: " + dataList.size()); + System.out.println("[查询去重] 去重处理完成"); + + return dataList; } @Override @@ -97,40 +259,215 @@ public class CattleDataServiceImpl implements CattleDataService { public ImportResult batchImportCattleData(List dataList) { List savedList = new ArrayList<>(); int newCount = 0; - int updateCount = 0; + int updateCount = 0; // 保留字段以兼容返回类型,但始终为0 + + // 确定导入日期:统一使用导入时的当前日期 + LocalDate importDate = LocalDate.now(); + LocalDateTime startOfDay = importDate.atStartOfDay(); // 当天 00:00:00 + LocalDateTime endOfDay = importDate.atTime(23, 59, 59, 999999999); // 当天 23:59:59.999999999 + + System.out.println("========== 开始导入数据,导入日期: " + importDate + " =========="); + + // 删除当天所有已存在的数据 + List todayData = cattleDataRepository.findByCreateTimeBetween(startOfDay, endOfDay); + if (todayData != null && !todayData.isEmpty()) { + System.out.println("========== 删除当天已存在的数据: " + todayData.size() + " 条 =========="); + cattleDataRepository.deleteAll(todayData); + } + + // 只新增,不做去重和更新 + LocalDateTime importDateTime = LocalDateTime.now(); // 统一使用导入时的当前时间 for (CattleDataDTO dto : dataList) { - // 检查是否存在重复数据(type + province + location + price) - CattleData existingData = cattleDataRepository - .findByTypeAndProvinceAndLocationAndPrice( - dto.getType(), - dto.getProvince(), - dto.getLocation(), - dto.getPrice() - ) - .orElse(null); - - if (existingData != null) { - // 如果存在重复数据,只更新 up_time(保留原有的 create_time) - existingData.setUpTime(LocalDateTime.now()); - savedList.add(cattleDataRepository.save(existingData)); - updateCount++; - } else { - // 如果不存在,创建新记录 - CattleData cattleData = new CattleData(); - cattleData.setType(dto.getType()); - cattleData.setProvince(dto.getProvince()); - cattleData.setLocation(dto.getLocation()); - cattleData.setPrice(dto.getPrice()); - if (dto.getCreateTime() != null) { - cattleData.setCreateTime(dto.getCreateTime()); - } - savedList.add(cattleDataRepository.save(cattleData)); - newCount++; + // 验证必填字段 + String type = dto.getType() != null ? dto.getType().trim() : null; + String location = dto.getLocation() != null ? dto.getLocation().trim() : null; + + if (type == null || type.isEmpty() || location == null || location.isEmpty()) { + continue; // 跳过无效数据 } + + // 创建新记录 + CattleData cattleData = new CattleData(); + cattleData.setType(type); + cattleData.setProvince(dto.getProvince() != null ? dto.getProvince().trim() : null); + cattleData.setLocation(location); + cattleData.setPrice(dto.getPrice()); + + // 统一设置create_time为导入时的当前时间 + cattleData.setCreateTime(importDateTime); + + CattleData saved = cattleDataRepository.save(cattleData); + savedList.add(saved); + newCount++; + System.out.println("[新增] 创建新记录: ID=" + saved.getId() + ", type=[" + type + "], location=[" + location + "], price=" + dto.getPrice()); } + System.out.println("========== 导入完成: 新增 " + newCount + " 条数据 =========="); + + // 导入完成后,计算当天省份的平均价格并更新省份每日均价 + System.out.println("========== 开始计算当天省份平均价格并更新每日均价 =========="); + calculateAndUpdateProvinceDailyPrices(importDate); + System.out.println("========== 省份平均价格计算完成 =========="); + return new ImportResult(savedList, newCount, updateCount); } + + /** + * 计算当天省份的平均价格并更新省份每日均价 + * 只计算当天导入的数据 + * + * @param importDate 导入日期 + */ + private void calculateAndUpdateProvinceDailyPrices(LocalDate importDate) { + try { + System.out.println("导入日期: " + importDate); + + // 获取所有15个省份(从 cattleprovince 表) + List allProvinces = cattleProvinceService.getAllProvinceData(null); + System.out.println("获取到 " + (allProvinces != null ? allProvinces.size() : 0) + " 个省份"); + + if (allProvinces == null || allProvinces.isEmpty()) { + System.out.println("没有找到省份数据,跳过省份平均价格计算"); + return; + } + + // 只查询当天导入的数据 + Sort sort = Sort.by(Sort.Direction.ASC, "price"); + List todayDataList = cattleDataRepository.findByCreateTimeDate(importDate, sort); + + if (todayDataList == null || todayDataList.isEmpty()) { + System.out.println("当天没有导入数据,跳过省份平均价格计算"); + return; + } + + System.out.println("当天导入的数据条数: " + todayDataList.size()); + + int updatedCount = 0; + int createdCount = 0; + + // 按省份分组,计算每个省份的平均价格 + java.util.Map> provinceStats = new java.util.HashMap<>(); + + for (CattleData data : todayDataList) { + if (data.getProvince() == null || data.getProvince().trim().isEmpty() || data.getPrice() == null) { + continue; + } + + String originalProvince = data.getProvince().trim(); + String canonicalProvince = resolveProvinceNameForMatch(originalProvince); + + if (provinceStats.containsKey(canonicalProvince)) { + java.util.Map existing = provinceStats.get(canonicalProvince); + java.math.BigDecimal existingSum = (java.math.BigDecimal) existing.get("sum"); + int existingCount = (Integer) existing.get("count"); + existingSum = existingSum.add(data.getPrice()); + existingCount++; + existing.put("sum", existingSum); + existing.put("count", existingCount); + } else { + java.util.Map stats = new java.util.HashMap<>(); + stats.put("sum", data.getPrice()); + stats.put("count", 1); + provinceStats.put(canonicalProvince, stats); + } + } + + // 计算每个省份的平均价格并更新 + for (java.util.Map.Entry> entry : provinceStats.entrySet()) { + String province = entry.getKey(); + java.util.Map stats = entry.getValue(); + java.math.BigDecimal totalSum = (java.math.BigDecimal) stats.get("sum"); + int totalCount = (Integer) stats.get("count"); + + // 计算平均值 + java.math.BigDecimal averagePrice = totalSum.divide( + java.math.BigDecimal.valueOf(totalCount), + 2, + java.math.RoundingMode.HALF_UP + ); + + System.out.println("省份: " + province + ", 当天数据条数: " + totalCount + ", 平均价格: " + averagePrice); + + // 更新 cattleprovince 表的 province_price 字段 + java.util.Optional cattleProvinceOpt = + cattleProvinceRepository.findFirstByProvince(province); + if (cattleProvinceOpt.isPresent()) { + com.example.cattletends.entity.CattleProvince cattleProvince = cattleProvinceOpt.get(); + cattleProvince.setProvincePrice(averagePrice); + cattleProvinceRepository.save(cattleProvince); + System.out.println("✓ 更新 cattleprovince 表的 province_price: " + province + ", 价格: " + averagePrice); + } + + // 更新或创建省份每日均价记录 + provinceDailyPriceService.saveOrUpdate(province, averagePrice, importDate); + updatedCount++; + System.out.println("✓ 更新省份每日均价: " + province + ", 日期: " + importDate + ", 价格: " + averagePrice); + } + + // 对于没有数据的省份,使用省份表中的省份均价(如果有) + for (com.example.cattletends.entity.CattleProvince province : allProvinces) { + String provinceName = province.getProvince(); + if (provinceName == null || provinceName.trim().isEmpty()) { + continue; + } + + String trimmedProvince = provinceName.trim(); + if (!provinceStats.containsKey(trimmedProvince)) { + // 没有当天数据,使用省份表中的省份均价 + if (province.getProvincePrice() != null) { + provinceDailyPriceService.saveOrUpdate(trimmedProvince, province.getProvincePrice(), importDate); + createdCount++; + System.out.println("✓ 创建省份每日均价(使用省份均价): " + trimmedProvince + ", 日期: " + importDate + ", 价格: " + province.getProvincePrice()); + } + } + } + + System.out.println("省份每日均价处理完成: 更新 " + updatedCount + " 条,创建 " + createdCount + " 条"); + + } catch (Exception e) { + System.err.println("计算省份平均价格失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 将牛只数据中的省份名称,转换为与 cattleprovince / province_daily_price 表中一致的标准省份名称 + * 例如:把“安徽省”“河北省”“新疆维吾尔自治区”等映射为“安徽”“河北”“新疆”等 + */ + private String resolveProvinceNameForMatch(String province) { + if (province == null) { + return null; + } + String trimmed = province.trim(); + + // 1. 先按原样查找 + java.util.Optional exact = + cattleProvinceRepository.findFirstByProvince(trimmed); + if (exact.isPresent()) { + return exact.get().getProvince(); + } + + // 2. 尝试去掉常见后缀(省、市、自治区、回族、壮族、维吾尔) + String shortName = trimmed + .replace("维吾尔", "") + .replace("回族", "") + .replace("壮族", "") + .replace("自治区", "") + .replace("省", "") + .replace("市", "") + .trim(); + + if (!shortName.equals(trimmed)) { + java.util.Optional shortMatch = + cattleProvinceRepository.findFirstByProvince(shortName); + if (shortMatch.isPresent()) { + return shortMatch.get().getProvince(); + } + } + + // 3. 找不到匹配时,退回原始名称(保证不会中断流程) + return trimmed; + } } diff --git a/backend/src/main/java/com/example/cattletends/service/impl/CattleNationalServiceImpl.java b/backend/src/main/java/com/example/cattletends/service/impl/CattleNationalServiceImpl.java index e40520d..20c46a7 100644 --- a/backend/src/main/java/com/example/cattletends/service/impl/CattleNationalServiceImpl.java +++ b/backend/src/main/java/com/example/cattletends/service/impl/CattleNationalServiceImpl.java @@ -57,7 +57,16 @@ public class CattleNationalServiceImpl implements CattleNationalService { int nationalSlaughter25th = 0; // 累加所有省份的数据 + System.out.println("========== 开始计算全国总量 =========="); + System.out.println("省份总数: " + allProvinces.size()); for (CattleProvince province : allProvinces) { + System.out.println("省份: " + province.getProvince() + + ", 23年存栏: " + province.getInventory23th() + + ", 23年出栏: " + province.getSlaughter23th() + + ", 24年存栏: " + province.getInventory24th() + + ", 24年出栏: " + province.getSlaughter24th() + + ", 25年存栏: " + province.getInventory25th() + + ", 25年出栏: " + province.getSlaughter25th()); if (province.getInventory23th() != null) { nationalInventory23th += province.getInventory23th(); } @@ -78,6 +87,14 @@ public class CattleNationalServiceImpl implements CattleNationalService { } } + System.out.println("计算结果: 23年存栏=" + nationalInventory23th + + ", 23年出栏=" + nationalSlaughter23th + + ", 24年存栏=" + nationalInventory24th + + ", 24年出栏=" + nationalSlaughter24th + + ", 25年存栏=" + nationalInventory25th + + ", 25年出栏=" + nationalSlaughter25th); + System.out.println("========== 计算完成 =========="); + // 查找是否已存在全国总量记录 CattleNational nationalData = cattleNationalRepository.findFirstByOrderByIdAsc() .orElse(null); diff --git a/backend/src/main/java/com/example/cattletends/service/impl/CattleProvinceServiceImpl.java b/backend/src/main/java/com/example/cattletends/service/impl/CattleProvinceServiceImpl.java index 5ad8c8a..d402608 100644 --- a/backend/src/main/java/com/example/cattletends/service/impl/CattleProvinceServiceImpl.java +++ b/backend/src/main/java/com/example/cattletends/service/impl/CattleProvinceServiceImpl.java @@ -47,12 +47,13 @@ public class CattleProvinceServiceImpl implements CattleProvinceService { continue; // 跳过无效数据 } // 查找该省份是否已存在 - CattleProvince provinceData = cattleProvinceRepository.findByProvince(dto.getProvince()) + CattleProvince provinceData = cattleProvinceRepository.findFirstByProvince(dto.getProvince()) .orElse(null); if (provinceData != null) { // 更新现有数据 - provinceData.setProvincePrice(dto.getProvincePrice()); + // 注意:不更新 provincePrice 字段,该字段只通过牛只数据导入时的计算得出 + provinceData.setProvincePriceRange(dto.getProvincePriceRange()); provinceData.setInventory23th(dto.getInventory23th()); provinceData.setSlaughter23th(dto.getSlaughter23th()); provinceData.setInventory24th(dto.getInventory24th()); @@ -65,7 +66,8 @@ public class CattleProvinceServiceImpl implements CattleProvinceService { // 创建新数据 provinceData = new CattleProvince(); provinceData.setProvince(dto.getProvince()); - provinceData.setProvincePrice(dto.getProvincePrice()); + // 注意:不设置 provincePrice 字段,该字段只通过牛只数据导入时的计算得出 + provinceData.setProvincePriceRange(dto.getProvincePriceRange()); provinceData.setInventory23th(dto.getInventory23th()); provinceData.setSlaughter23th(dto.getSlaughter23th()); provinceData.setInventory24th(dto.getInventory24th()); @@ -109,24 +111,189 @@ public class CattleProvinceServiceImpl implements CattleProvinceService { } @Override - @Transactional(readOnly = true) + @Transactional public List getAllProvinceData(String province) { // 按省份均价降序排序 Sort sort = Sort.by(Sort.Direction.DESC, "provincePrice"); + List dataList; + // 如果提供了省份参数,则只查询该省份数据 if (province != null && !province.trim().isEmpty()) { - return cattleProvinceRepository.findByProvince(province.trim(), sort); + dataList = cattleProvinceRepository.findByProvince(province.trim(), sort); + } else { + // 否则返回所有省份数据,按省份均价降序排序 + dataList = cattleProvinceRepository.findAll(sort); } - // 否则返回所有省份数据,按省份均价降序排序 - return cattleProvinceRepository.findAll(sort); + // 去重处理:如果同一个省份有多条记录,保留最新的一条(up_time最大),更新其他记录 + if (dataList != null && !dataList.isEmpty()) { + System.out.println("========== 开始去重处理 =========="); + System.out.println("去重前记录数: " + dataList.size()); + + // 按省份分组,找出每个省份的最新记录 + java.util.Map provinceMap = new java.util.HashMap<>(); + java.util.List duplicatesToUpdate = new java.util.ArrayList<>(); + + for (CattleProvince item : dataList) { + String provinceName = item.getProvince(); + if (provinceName == null || provinceName.trim().isEmpty()) { + continue; + } + + if (!provinceMap.containsKey(provinceName)) { + // 第一次遇到该省份,直接添加 + provinceMap.put(provinceName, item); + } else { + // 已存在该省份,比较更新时间,保留最新的 + CattleProvince existing = provinceMap.get(provinceName); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + // 当前记录更新,标记旧记录需要更新 + duplicatesToUpdate.add(existing); + provinceMap.put(provinceName, item); + } else { + // 现有记录更新,标记当前记录需要更新 + duplicatesToUpdate.add(item); + } + } else if (item.getUpTime() != null) { + // 当前记录有更新时间,保留它 + duplicatesToUpdate.add(existing); + provinceMap.put(provinceName, item); + } else if (existing.getUpTime() != null) { + // 现有记录有更新时间,保留现有记录 + duplicatesToUpdate.add(item); + } else { + // 都没有更新时间,保留ID较小的(通常是最早创建的) + if (item.getId() < existing.getId()) { + duplicatesToUpdate.add(existing); + provinceMap.put(provinceName, item); + } else { + duplicatesToUpdate.add(item); + } + } + } + } + + // 更新重复记录:将旧记录的数据更新为最新记录的数据 + int updatedCount = 0; + for (CattleProvince duplicate : duplicatesToUpdate) { + CattleProvince latest = provinceMap.get(duplicate.getProvince()); + if (latest != null && !latest.getId().equals(duplicate.getId())) { + // 更新旧记录的数据为最新记录的数据 + duplicate.setProvincePrice(latest.getProvincePrice()); + duplicate.setProvincePriceRange(latest.getProvincePriceRange()); + duplicate.setInventory23th(latest.getInventory23th()); + duplicate.setSlaughter23th(latest.getSlaughter23th()); + duplicate.setInventory24th(latest.getInventory24th()); + duplicate.setSlaughter24th(latest.getSlaughter24th()); + duplicate.setInventory25th(latest.getInventory25th()); + duplicate.setSlaughter25th(latest.getSlaughter25th()); + cattleProvinceRepository.save(duplicate); + updatedCount++; + } + } + + // 如果更新了重复记录,重新查询 + if (updatedCount > 0) { + System.out.println("更新了 " + updatedCount + " 条重复记录"); + if (province != null && !province.trim().isEmpty()) { + dataList = cattleProvinceRepository.findByProvince(province.trim(), sort); + } else { + dataList = cattleProvinceRepository.findAll(sort); + } + } + + // 最终去重:按省份去重,每个省份只保留一条(保留最新的) + java.util.Map finalMap = new java.util.LinkedHashMap<>(); + for (CattleProvince item : dataList) { + String provinceName = item.getProvince(); + if (provinceName == null || provinceName.trim().isEmpty()) { + continue; + } + + if (!finalMap.containsKey(provinceName)) { + finalMap.put(provinceName, item); + } else { + CattleProvince existing = finalMap.get(provinceName); + if (item.getUpTime() != null && existing.getUpTime() != null) { + if (item.getUpTime().isAfter(existing.getUpTime())) { + finalMap.put(provinceName, item); + } + } else if (item.getUpTime() != null) { + finalMap.put(provinceName, item); + } else if (existing.getUpTime() != null) { + // 保持现有记录 + } else { + // 都没有更新时间,保留ID较小的 + if (item.getId() < existing.getId()) { + finalMap.put(provinceName, item); + } + } + } + } + + dataList = new java.util.ArrayList<>(finalMap.values()); + System.out.println("去重后记录数: " + dataList.size()); + System.out.println("========== 去重处理完成 =========="); + } + + return dataList; } @Override - @Transactional(readOnly = true) + @Transactional public CattleProvince getProvinceDataByName(String province) { - return cattleProvinceRepository.findByProvince(province) - .orElseThrow(() -> new RuntimeException("省份数据不存在:" + province)); + // 查询该省份的所有记录 + List provinceList = cattleProvinceRepository.findAllByProvince(province); + + if (provinceList == null || provinceList.isEmpty()) { + throw new RuntimeException("省份数据不存在:" + province); + } + + // 如果有多条记录,进行去重处理 + if (provinceList.size() > 1) { + System.out.println("========== 发现重复省份记录: " + province + " =========="); + System.out.println("重复记录数: " + provinceList.size()); + + // 找出最新的记录(up_time最大) + CattleProvince latest = provinceList.get(0); + for (CattleProvince item : provinceList) { + if (item.getUpTime() != null && latest.getUpTime() != null) { + if (item.getUpTime().isAfter(latest.getUpTime())) { + latest = item; + } + } else if (item.getUpTime() != null) { + latest = item; + } + } + + // 更新其他重复记录 + int updatedCount = 0; + for (CattleProvince item : provinceList) { + if (!item.getId().equals(latest.getId())) { + // 更新旧记录的数据为最新记录的数据 + item.setProvincePrice(latest.getProvincePrice()); + item.setProvincePriceRange(latest.getProvincePriceRange()); + item.setInventory23th(latest.getInventory23th()); + item.setSlaughter23th(latest.getSlaughter23th()); + item.setInventory24th(latest.getInventory24th()); + item.setSlaughter24th(latest.getSlaughter24th()); + item.setInventory25th(latest.getInventory25th()); + item.setSlaughter25th(latest.getSlaughter25th()); + cattleProvinceRepository.save(item); + updatedCount++; + } + } + + if (updatedCount > 0) { + System.out.println("更新了 " + updatedCount + " 条重复记录"); + } + + return latest; + } + + // 只有一条记录,直接返回 + return provinceList.get(0); } } diff --git a/backend/src/main/java/com/example/cattletends/service/impl/ProvinceDailyPriceServiceImpl.java b/backend/src/main/java/com/example/cattletends/service/impl/ProvinceDailyPriceServiceImpl.java new file mode 100644 index 0000000..ba0b60f --- /dev/null +++ b/backend/src/main/java/com/example/cattletends/service/impl/ProvinceDailyPriceServiceImpl.java @@ -0,0 +1,94 @@ +package com.example.cattletends.service.impl; + +import com.example.cattletends.entity.ProvinceDailyPrice; +import com.example.cattletends.repository.ProvinceDailyPriceRepository; +import com.example.cattletends.service.ProvinceDailyPriceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +/** + * 省份每日均价服务实现类 + */ +@Service +public class ProvinceDailyPriceServiceImpl implements ProvinceDailyPriceService { + + @Autowired + private ProvinceDailyPriceRepository provinceDailyPriceRepository; + + @Override + @Transactional + public ProvinceDailyPrice saveOrUpdate(String province, java.math.BigDecimal price, LocalDate priceDate) { + if (province == null || province.trim().isEmpty() || price == null) { + System.out.println("保存省份每日均价失败: 参数不完整 - 省份: " + province + ", 价格: " + price); + return null; + } + + String trimmedProvince = province.trim(); + LocalDate date = priceDate != null ? priceDate : LocalDate.now(); + + // 检查是否已存在(同一天同一省份) + java.util.Optional existing = provinceDailyPriceRepository.findByProvinceAndPriceDate(trimmedProvince, date); + + if (existing.isPresent()) { + // 已存在,更新价格 + ProvinceDailyPrice dailyPrice = existing.get(); + java.math.BigDecimal oldPrice = dailyPrice.getPrice(); + dailyPrice.setPrice(price); + ProvinceDailyPrice updated = provinceDailyPriceRepository.save(dailyPrice); + System.out.println("✓ 更新省份每日均价: " + trimmedProvince + ", 日期: " + date + ", 旧价格: " + oldPrice + " -> 新价格: " + price + " (ID: " + updated.getId() + ")"); + return updated; + } else { + // 不存在则创建新记录 + ProvinceDailyPrice dailyPrice = new ProvinceDailyPrice(); + dailyPrice.setProvince(trimmedProvince); + dailyPrice.setPrice(price); + dailyPrice.setPriceDate(date); + + ProvinceDailyPrice saved = provinceDailyPriceRepository.save(dailyPrice); + System.out.println("✓ 保存省份每日均价: " + trimmedProvince + ", 日期: " + date + ", 价格: " + price + " (ID: " + saved.getId() + ")"); + return saved; + } + } + + @Override + public List getByProvince(String province) { + if (province == null || province.trim().isEmpty()) { + return provinceDailyPriceRepository.findAllByOrderByPriceDateDesc(); + } + return provinceDailyPriceRepository.findByProvinceOrderByPriceDateDesc(province.trim()); + } + + @Override + public List getAll() { + return provinceDailyPriceRepository.findAllByOrderByPriceDateDesc(); + } + + @Override + public List getByProvinceAndDateRange(String province, LocalDate startDate, LocalDate endDate) { + if (province == null || province.trim().isEmpty()) { + return getAll(); + } + return provinceDailyPriceRepository.findByProvinceAndDateRange(province.trim(), startDate, endDate); + } + + @Override + public List getByPriceDate(LocalDate priceDate) { + if (priceDate == null) { + return getAll(); + } + return provinceDailyPriceRepository.findByPriceDateOrderByProvinceAsc(priceDate); + } + + @Override + public java.util.Optional getByProvinceAndPriceDate(String province, LocalDate priceDate) { + if (province == null || province.trim().isEmpty() || priceDate == null) { + return java.util.Optional.empty(); + } + return provinceDailyPriceRepository.findByProvinceAndPriceDate(province.trim(), priceDate); + } +} + diff --git a/backend/src/main/resources/db/migration/add_indexes_for_province_daily_price.sql b/backend/src/main/resources/db/migration/add_indexes_for_province_daily_price.sql new file mode 100644 index 0000000..2966d60 --- /dev/null +++ b/backend/src/main/resources/db/migration/add_indexes_for_province_daily_price.sql @@ -0,0 +1,37 @@ +-- 为 province_daily_price 表添加索引以优化 SQL 查询速度 +-- 注意:如果索引已存在,执行会报错(Duplicate key name),可以忽略或先检查再创建 + +-- 检查索引是否存在的存储过程(可选,用于安全创建) +-- 如果不想使用存储过程,可以直接执行下面的 CREATE INDEX 语句 +-- 如果索引已存在,会报错但不会影响其他索引的创建 + +-- 1. 复合索引:省份 + 日期(用于按省份和日期范围查询和排序) +-- 注意:虽然已有 UNIQUE KEY `uk_province_date`,但添加单独的索引可以优化某些查询场景 +CREATE INDEX `idx_province_date_composite` ON `province_daily_price` (`province`, `price_date` DESC); + +-- 2. 价格索引(用于按价格范围查询和排序) +CREATE INDEX `idx_price` ON `province_daily_price` (`price`); + +-- 3. 复合索引:日期 + 价格(用于按日期和价格范围查询) +CREATE INDEX `idx_date_price` ON `province_daily_price` (`price_date`, `price`); + +-- 4. 复合索引:省份 + 价格(用于按省份和价格范围查询) +CREATE INDEX `idx_province_price` ON `province_daily_price` (`province`, `price`); + +-- 5. 更新时间索引(用于按更新时间排序和查询) +CREATE INDEX `idx_up_time` ON `province_daily_price` (`up_time`); + +-- 6. 创建时间索引(用于按创建时间排序和查询) +CREATE INDEX `idx_create_time` ON `province_daily_price` (`create_time`); + +-- 验证索引 +-- SHOW INDEX FROM province_daily_price; + +-- 查看表结构 +-- SHOW CREATE TABLE province_daily_price; + +-- 查看索引使用情况(需要先执行一些查询) +-- EXPLAIN SELECT * FROM province_daily_price WHERE province = '内蒙古' ORDER BY price_date DESC; +-- EXPLAIN SELECT * FROM province_daily_price WHERE price_date BETWEEN '2025-12-01' AND '2025-12-07'; +-- EXPLAIN SELECT * FROM province_daily_price WHERE province = '内蒙古' AND price BETWEEN 13.0 AND 15.0; + diff --git a/backend/src/main/resources/db/migration/add_is_delet_to_cattlebreed.sql b/backend/src/main/resources/db/migration/add_is_delet_to_cattlebreed.sql new file mode 100644 index 0000000..bd594d9 --- /dev/null +++ b/backend/src/main/resources/db/migration/add_is_delet_to_cattlebreed.sql @@ -0,0 +1,13 @@ +-- 为 cattlebreed 表添加 is_delet 字段(逻辑删除标志) +-- 0 = 未删除,1 = 已删除 + +-- 检查字段是否存在,如果不存在则添加 +-- 注意:MySQL 不支持 IF NOT EXISTS,需要先检查 +-- 如果字段已存在,执行此脚本会报错,可以忽略 + +ALTER TABLE cattlebreed +ADD COLUMN is_delet INT NOT NULL DEFAULT 0 COMMENT '逻辑删除标志:0=未删除,1=已删除'; + +-- 为 is_delet 字段添加索引,优化查询性能 +CREATE INDEX idx_is_delet ON cattlebreed(is_delet); + diff --git a/backend/src/main/resources/db/migration/create_cattlebreed_table.sql b/backend/src/main/resources/db/migration/create_cattlebreed_table.sql index 8bc47a8..530642a 100644 --- a/backend/src/main/resources/db/migration/create_cattlebreed_table.sql +++ b/backend/src/main/resources/db/migration/create_cattlebreed_table.sql @@ -1,12 +1,10 @@ -- 创建品种表 CREATE TABLE IF NOT EXISTS `cattlebreed` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', - `breed_name` VARCHAR(100) NOT NULL UNIQUE COMMENT '品种名称', - `create_time` DATETIME COMMENT '创建时间', - `up_time` DATETIME NOT NULL COMMENT '更新时间', + -- 索引定义 INDEX `idx_breed_name` (`breed_name`), INDEX `idx_create_time` (`create_time`), - INDEX `idx_up_time` (`up_time`) + INDEX `idx_up_time` (`up_time`), + INDEX `idx_is_delet` (`is_delet`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品种表'; diff --git a/backend/src/main/resources/db/migration/create_province_daily_price_table.sql b/backend/src/main/resources/db/migration/create_province_daily_price_table.sql new file mode 100644 index 0000000..e6f0625 --- /dev/null +++ b/backend/src/main/resources/db/migration/create_province_daily_price_table.sql @@ -0,0 +1,15 @@ +-- 创建省份每日均价表 +-- 存储15个省份每天的省份均价,只累加不覆盖 +CREATE TABLE IF NOT EXISTS `province_daily_price` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + `province` VARCHAR(100) NOT NULL COMMENT '省份名称', + `price` DECIMAL(10, 2) NOT NULL COMMENT '省份均价(元/斤)', + `price_date` DATE NOT NULL COMMENT '价格日期', + `create_time` DATETIME COMMENT '创建时间', + `up_time` DATETIME NOT NULL COMMENT '更新时间', + -- 索引定义 + UNIQUE KEY `uk_province_date` (`province`, `price_date`), + INDEX `idx_province` (`province`), + INDEX `idx_price_date` (`price_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省份每日均价表(只累加不覆盖)'; + diff --git a/backend/src/main/resources/db/migration/generate_province_daily_price_last_7_days.sql b/backend/src/main/resources/db/migration/generate_province_daily_price_last_7_days.sql new file mode 100644 index 0000000..dd1a060 --- /dev/null +++ b/backend/src/main/resources/db/migration/generate_province_daily_price_last_7_days.sql @@ -0,0 +1,62 @@ +-- 为 province_daily_price 表生成前7天的15个省份数据 +-- 价格在基准价格(从 cattleprovince 表获取)上下浮动1元之内 +-- 使用 INSERT IGNORE 避免重复插入(如果某天某省份已有数据则跳过) + +-- 生成前7天的数据(包括今天,共7天:今天、昨天、前天...) +INSERT IGNORE INTO `province_daily_price` (`province`, `price`, `price_date`, `create_time`, `up_time`) +SELECT + cp.province, + -- 价格浮动:基准价格 + (-1.00 到 +1.00 之间的随机值) + -- 使用 ROUND() 保留2位小数 + -- 使用 GREATEST() 确保价格不小于 0.01 + ROUND( + GREATEST(0.01, + cp.province_price + (RAND() * 2 - 1) -- RAND() * 2 - 1 生成 -1 到 +1 之间的值 + ), + 2 + ) as price, + DATE_SUB(CURDATE(), INTERVAL day_offset DAY) as price_date, + NOW() as create_time, + NOW() as up_time +FROM + (SELECT DISTINCT province, province_price + FROM cattleprovince + WHERE province_price IS NOT NULL + ORDER BY province) cp +CROSS JOIN + (SELECT 0 as day_offset UNION ALL + SELECT 1 UNION ALL + SELECT 2 UNION ALL + SELECT 3 UNION ALL + SELECT 4 UNION ALL + SELECT 5 UNION ALL + SELECT 6) days +ORDER BY cp.province, day_offset; + +-- 验证生成的数据 +-- 查看前7天每个省份的价格和与基准价格的差值 +SELECT + pdp.province, + pdp.price_date, + pdp.price, + cp.province_price as base_price, + ROUND(pdp.price - cp.province_price, 2) as price_diff, + CASE + WHEN ABS(pdp.price - cp.province_price) <= 1.0 THEN '✓ 在范围内' + ELSE '✗ 超出范围' + END as status +FROM province_daily_price pdp +INNER JOIN cattleprovince cp ON pdp.province = cp.province +WHERE pdp.price_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + AND pdp.price_date <= CURDATE() +ORDER BY pdp.province, pdp.price_date DESC; + +-- 统计信息 +-- SELECT +-- COUNT(*) as total_records, +-- COUNT(DISTINCT province) as province_count, +-- MIN(price_date) as earliest_date, +-- MAX(price_date) as latest_date +-- FROM province_daily_price +-- WHERE price_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY); + diff --git a/province_daily_price_insert.sql b/province_daily_price_insert.sql new file mode 100644 index 0000000..f9277c7 --- /dev/null +++ b/province_daily_price_insert.sql @@ -0,0 +1,93 @@ +-- 生成12月3日之前5天的省份每日均价数据 +-- 价格在12月3日价格基础上上下浮动不超过1元 + +-- 11月28日数据 +INSERT INTO province_daily_price (province, price, price_date, create_time, up_time) VALUES +('新疆', 12.85, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('河北', 8.15, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('山东', 9.68, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('甘肃', 10.35, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('吉林', 10.82, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('青海', 11.25, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('黑龙江', 11.45, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('安徽', 11.58, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('贵州', 11.92, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('内蒙古', 12.28, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('西藏', 12.45, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('四川', 12.48, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('宁夏', 12.55, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('云南', 12.85, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'), +('广西', 13.35, '2025-11-28', '2025-11-28 09:00:00', '2025-11-28 09:00:00'); + +-- 11月29日数据 +INSERT INTO province_daily_price (province, price, price_date, create_time, up_time) VALUES +('新疆', 13.05, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('河北', 7.65, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('山东', 9.15, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('甘肃', 9.85, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('吉林', 10.25, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('青海', 10.65, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('黑龙江', 10.95, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('安徽', 11.05, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('贵州', 11.35, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('内蒙古', 11.65, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('西藏', 11.85, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('四川', 11.88, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('宁夏', 11.95, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('云南', 12.25, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'), +('广西', 12.75, '2025-11-29', '2025-11-29 09:00:00', '2025-11-29 09:00:00'); + +-- 11月30日数据 +INSERT INTO province_daily_price (province, price, price_date, create_time, up_time) VALUES +('新疆', 13.28, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('河北', 8.25, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('山东', 9.45, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('甘肃', 9.95, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('吉林', 10.35, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('青海', 10.75, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('黑龙江', 11.05, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('安徽', 11.15, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('贵州', 11.45, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('内蒙古', 11.75, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('西藏', 11.95, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('四川', 11.98, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('宁夏', 12.05, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('云南', 12.35, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'), +('广西', 12.85, '2025-11-30', '2025-11-30 09:00:00', '2025-11-30 09:00:00'); + +-- 12月1日数据 +INSERT INTO province_daily_price (province, price, price_date, create_time, up_time) VALUES +('新疆', 13.05, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('河北', 7.75, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('山东', 9.25, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('甘肃', 9.85, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('吉林', 10.25, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('青海', 10.65, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('黑龙江', 10.95, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('安徽', 11.05, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('贵州', 11.35, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('内蒙古', 11.65, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('西藏', 11.85, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('四川', 11.88, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('宁夏', 11.95, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('云南', 12.25, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'), +('广西', 12.75, '2025-12-01', '2025-12-01 09:00:00', '2025-12-01 09:00:00'); + +-- 12月2日数据 +INSERT INTO province_daily_price (province, price, price_date, create_time, up_time) VALUES +('新疆', 13.18, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('河北', 8.05, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('山东', 9.38, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('甘肃', 9.88, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('吉林', 10.38, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('青海', 10.78, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('黑龙江', 11.08, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('安徽', 11.18, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('贵州', 11.48, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('内蒙古', 11.78, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('西藏', 11.98, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('四川', 12.03, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('宁夏', 12.10, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('云南', 12.40, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'), +('广西', 12.90, '2025-12-02', '2025-12-02 09:00:00', '2025-12-02 09:00:00'); +