HANDOFF.md
@@ -6,6 +6,14 @@ ## 最新(2026-10-06) ### 10-06:往年国省道调整列表补充调整前值 - **改动**:调整列表接口在同一年份、同一日期、普通国省道口径下,从 `holiday_vehicle_flow` 补返回 `originalValue`;没有原始记录时保持空,页面不显示占位符。表格新增“调整前国省道车流量(万辆次)”,原“调整后国省道车流”改为“调整后国省道车流量”。 - **弹窗**:新增/编辑弹窗同步增加只读“调整前国省道车流量(万辆次)”;编辑已有记录时展示 `originalValue`,无原值或新选择日期时不显示内容。切换年份或日期会清空旧原值,避免错日展示。 - **随日期刷新**:新增接口 `GET /api/holiday/adjustments/source-value`,按所选年份和日期实时读取普通国省道原值;新增/编辑弹窗选日期后自动刷新,未查到源数据时显示为空。前端用请求序号丢弃迟到的旧请求,避免快速换日期时串值。 - **边界**:`originalValue` 只是列表展示字段,标记为 `@TableField(exist = false)`,不写库、不参与调整、环比分母、累计或走势图计算。 - **验证**:`mvn -o test` 11 个测试全部通过;`npm run build` 通过;本地重启后只读核对 2025-10-03/04/05 原值分别为 `362.0035/337.9501/334.2040`,无源数据的 2025-10-09 返回空。测试报告:`docs/功能测试报告/功能测试报告_往年国省道调整原值展示_2026-10-06.md`(本地留存,不入库)。 ### 10-06:方案 D 已实现(往年国省道调整表 + 环比基期只作用于前一日分母) - **最终口径**:`HolidayStatService.loadVehicleByDate` 先用 `holiday_vehicle_adjustment` 覆盖对应历史年份的“当日值”,供走势图、同比、累计和快报使用;随后构造“前一日环比分母”,若前一日普通国省道 C 列有值,则用 C 替换该日前一日国省道值,并据此重算非营业性人员流动量、公路人员流动量和总计。所有环比行只继承这份分母。 traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java
@@ -13,6 +13,8 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -30,6 +32,16 @@ return Result.ok(adjustmentService.list(year)); } @GetMapping("/source-value") public Result<BigDecimal> sourceValue(@RequestParam("year") Integer year, @RequestParam("flowDate") String flowDate) { try { return Result.ok(adjustmentService.findOriginalValue(year, LocalDate.parse(flowDate))); } catch (Exception e) { return Result.error(400, e.getMessage()); } } @PostMapping("/save") public Result<HolidayVehicleAdjustment> save(@RequestHeader(value = "Authorization", required = false) String auth, @RequestBody Map<String, Object> params) { traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleAdjustment.java
@@ -1,6 +1,7 @@ package com.trafficaudit.holiday.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -17,6 +18,8 @@ private Integer year; private LocalDate flowDate; private BigDecimal adjustedValue; @TableField(exist = false) private BigDecimal originalValue; private String reason; private LocalDateTime createdAt; private LocalDateTime updatedAt; traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java
@@ -1,8 +1,11 @@ package com.trafficaudit.holiday.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.trafficaudit.holiday.HolidayConstants; import com.trafficaudit.holiday.HolidayNumberUtils; import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; import com.trafficaudit.holiday.entity.HolidayVehicleFlow; import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,8 +14,11 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @Service public class HolidayVehicleAdjustmentService { @@ -21,12 +27,55 @@ @Resource private HolidayVehicleAdjustmentMapper adjustmentMapper; @Resource private HolidayVehicleFlowMapper vehicleFlowMapper; public List<HolidayVehicleAdjustment> list(Integer year) { return adjustmentMapper.selectList(new LambdaQueryWrapper<HolidayVehicleAdjustment>() .eq(year != null, HolidayVehicleAdjustment::getYear, year) .orderByDesc(HolidayVehicleAdjustment::getYear) .orderByAsc(HolidayVehicleAdjustment::getFlowDate)); List<HolidayVehicleAdjustment> rows = adjustmentMapper.selectList( new LambdaQueryWrapper<HolidayVehicleAdjustment>() .eq(year != null, HolidayVehicleAdjustment::getYear, year) .orderByDesc(HolidayVehicleAdjustment::getYear) .orderByAsc(HolidayVehicleAdjustment::getFlowDate)); if (rows.isEmpty()) return rows; Set<Integer> years = new HashSet<>(); for (HolidayVehicleAdjustment row : rows) { if (row.getYear() != null) years.add(row.getYear()); } if (years.isEmpty()) return rows; List<HolidayVehicleFlow> sourceRows = vehicleFlowMapper.selectList( new LambdaQueryWrapper<HolidayVehicleFlow>() .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL) .eq(HolidayVehicleFlow::getVehicleType, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER) .in(HolidayVehicleFlow::getYear, years)); Map<String, BigDecimal> originalValues = new HashMap<>(); for (HolidayVehicleFlow sourceRow : sourceRows) { if (sourceRow.getYear() == null || sourceRow.getFlowDate() == null || sourceRow.getVehicleCount() == null) { continue; } originalValues.put(sourceKey(sourceRow.getYear(), sourceRow.getFlowDate()), sourceRow.getVehicleCount()); } for (HolidayVehicleAdjustment row : rows) { if (row.getYear() != null && row.getFlowDate() != null) { row.setOriginalValue(originalValues.get(sourceKey(row.getYear(), row.getFlowDate()))); } } return rows; } public BigDecimal findOriginalValue(Integer year, LocalDate flowDate) { if (year == null || flowDate == null || flowDate.getYear() != year) { throw new IllegalArgumentException("年份和日期不正确"); } HolidayVehicleFlow sourceRow = vehicleFlowMapper.selectOne( new LambdaQueryWrapper<HolidayVehicleFlow>() .eq(HolidayVehicleFlow::getYear, year) .eq(HolidayVehicleFlow::getFlowDate, flowDate) .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL) .eq(HolidayVehicleFlow::getVehicleType, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER) .last("limit 1")); return sourceRow == null ? null : sourceRow.getVehicleCount(); } @Transactional @@ -98,6 +147,10 @@ adjustmentMapper.deleteById(id); } private String sourceKey(Integer year, LocalDate flowDate) { return year + "|" + flowDate; } private Long longValue(Object value) { if (value == null || value.toString().trim().isEmpty()) return null; return Long.valueOf(value.toString().trim()); traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentServiceTest.java
New file @@ -0,0 +1,85 @@ package com.trafficaudit.holiday.service; import com.trafficaudit.holiday.HolidayConstants; import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; import com.trafficaudit.holiday.entity.HolidayVehicleFlow; import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class HolidayVehicleAdjustmentServiceTest { @Test void listAddsOriginalNationalProvincialValueAndLeavesMissingSourceBlank() throws Exception { HolidayVehicleAdjustment adjusted = adjustment("2025-10-04", "337.5009"); HolidayVehicleAdjustment missingSource = adjustment("2025-10-05", "330.0000"); HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class); HolidayVehicleFlowMapper vehicleFlowMapper = mock(HolidayVehicleFlowMapper.class); when(adjustmentMapper.selectList(any())).thenReturn(Arrays.asList(adjusted, missingSource)); when(vehicleFlowMapper.selectList(any())).thenReturn(Collections.singletonList( vehicleFlow("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000"))); HolidayVehicleAdjustmentService service = new HolidayVehicleAdjustmentService(); inject(service, "adjustmentMapper", adjustmentMapper); inject(service, "vehicleFlowMapper", vehicleFlowMapper); List<HolidayVehicleAdjustment> rows = service.list(2025); assertEquals(0, new BigDecimal("350.0000").compareTo(rows.get(0).getOriginalValue())); assertNull(rows.get(1).getOriginalValue()); } @Test void findOriginalValueReturnsMatchedValueAndNullWhenMissing() throws Exception { HolidayVehicleFlowMapper vehicleFlowMapper = mock(HolidayVehicleFlowMapper.class); when(vehicleFlowMapper.selectOne(any())) .thenReturn(vehicleFlow("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "337.9501")) .thenReturn(null); HolidayVehicleAdjustmentService service = new HolidayVehicleAdjustmentService(); inject(service, "vehicleFlowMapper", vehicleFlowMapper); assertEquals(0, new BigDecimal("337.9501") .compareTo(service.findOriginalValue(2025, LocalDate.of(2025, 10, 4)))); assertNull(service.findOriginalValue(2025, LocalDate.of(2025, 10, 5))); assertThrows(IllegalArgumentException.class, () -> service.findOriginalValue(2025, LocalDate.of(2024, 10, 4))); } private static HolidayVehicleAdjustment adjustment(String date, String adjustedValue) { HolidayVehicleAdjustment row = new HolidayVehicleAdjustment(); row.setYear(LocalDate.parse(date).getYear()); row.setFlowDate(LocalDate.parse(date)); row.setAdjustedValue(new BigDecimal(adjustedValue)); return row; } private static HolidayVehicleFlow vehicleFlow(String date, String roadCategory, String value) { HolidayVehicleFlow row = new HolidayVehicleFlow(); row.setYear(LocalDate.parse(date).getYear()); row.setFlowDate(LocalDate.parse(date)); row.setRoadCategory(roadCategory); row.setVehicleCount(new BigDecimal(value)); return row; } private static void inject(Object target, String fieldName, Object value) throws Exception { Field field = target.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(target, value); } } traffic-audit-web/src/views/HolidayStat.vue
@@ -100,7 +100,10 @@ <el-table :data="adjustments" border stripe size="small" style="margin-top:12px" v-loading="adjustmentLoading"> <el-table-column prop="year" label="年份" width="90" align="center"></el-table-column> <el-table-column prop="flowDate" label="日期" width="130" align="center"></el-table-column> <el-table-column label="调整后国省道车流(万辆次)" width="230" align="right"> <el-table-column label="调整前国省道车流量(万辆次)" width="240" align="right"> <template slot-scope="s">{{ formatOriginalAdjustmentValue(s.row.originalValue) }}</template> </el-table-column> <el-table-column label="调整后国省道车流量(万辆次)" width="240" align="right"> <template slot-scope="s">{{ formatAdjustmentValue(s.row.adjustedValue) }}</template> </el-table-column> <el-table-column prop="reason" label="调整原因" min-width="280" show-overflow-tooltip></el-table-column> @@ -113,15 +116,20 @@ </el-table-column> </el-table> <el-dialog :title="adjustmentForm.id ? '编辑调整' : '新增调整'" :visible.sync="adjustmentDialogVisible" width="560px" v-dialog-drag> <el-form label-width="160px"> <el-form label-width="220px"> <el-form-item label="年份"> <el-select v-model="adjustmentForm.year" style="width:100%" @change="adjustmentForm.flowDate = ''"> <el-select v-model="adjustmentForm.year" style="width:100%" @change="onAdjustmentYearChange"> <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + '年'" :value="item"></el-option> </el-select> </el-form-item> <el-form-item label="日期"> <el-date-picker v-model="adjustmentForm.flowDate" type="date" value-format="yyyy-MM-dd" style="width:100%" :picker-options="adjustmentDatePickerOptions" placeholder="选择日期"></el-date-picker> style="width:100%" :picker-options="adjustmentDatePickerOptions" placeholder="选择日期" @change="onAdjustmentDateChange"></el-date-picker> </el-form-item> <el-form-item label="调整前国省道车流量(万辆次)"> <el-input :value="formatOriginalAdjustmentValue(adjustmentForm.originalValue)" readonly :placeholder="adjustmentOriginalLoading ? '查询中...' : ''"></el-input> </el-form-item> <el-form-item label="调整后车流量(万辆次)"> <el-input-number v-model="adjustmentForm.adjustedValue" :min="0" :precision="4" :step="0.0001" controls-position="right" style="width:100%"></el-input-number> @@ -249,10 +257,13 @@ adjustmentLoading: false, adjustmentDialogVisible: false, adjustmentSaving: false, adjustmentOriginalLoading: false, adjustmentOriginalRequestId: 0, adjustmentForm: { id: null, year: new Date().getFullYear() - 1, flowDate: '', originalValue: null, adjustedValue: null, reason: '' } @@ -319,11 +330,49 @@ id: source.id || null, year: source.year || this.adjustmentYear, flowDate: source.flowDate || '', originalValue: source.originalValue === undefined || source.originalValue === null ? null : Number(source.originalValue), adjustedValue: source.adjustedValue === undefined || source.adjustedValue === null ? null : Number(source.adjustedValue), reason: source.reason || '' } this.adjustmentDialogVisible = true this.loadAdjustmentOriginalValue() }, onAdjustmentYearChange() { this.adjustmentOriginalRequestId += 1 this.adjustmentOriginalLoading = false this.adjustmentForm.flowDate = '' this.adjustmentForm.originalValue = null }, onAdjustmentDateChange() { this.loadAdjustmentOriginalValue() }, loadAdjustmentOriginalValue() { const year = this.adjustmentForm.year const flowDate = this.adjustmentForm.flowDate const requestId = ++this.adjustmentOriginalRequestId this.adjustmentForm.originalValue = null if (!year || !flowDate) { this.adjustmentOriginalLoading = false return } this.adjustmentOriginalLoading = true api.get('/holiday/adjustments/source-value', { params: { year: year, flowDate: flowDate } }) .then(res => { if (requestId !== this.adjustmentOriginalRequestId) return if (res.code !== 200) throw new Error(res.message || '调整前值加载失败') this.adjustmentForm.originalValue = res.data === null || res.data === undefined || res.data === '' ? null : Number(res.data) }) .catch(err => { if (requestId !== this.adjustmentOriginalRequestId) return this.adjustmentForm.originalValue = null this.showError(err, '调整前值加载失败') }) .finally(() => { if (requestId === this.adjustmentOriginalRequestId) this.adjustmentOriginalLoading = false }) }, saveAdjustment() { const form = this.adjustmentForm @@ -582,6 +631,10 @@ if (!Number.isFinite(number)) return value return number.toFixed(4) }, formatOriginalAdjustmentValue(value) { if (value === null || value === undefined || value === '') return '' return this.formatAdjustmentValue(value) }, formatDateTime(value) { if (!value) return '—' return String(value).replace('T', ' ').slice(0, 19)