xyc
昨天 b425da0611b39f20fc868645c59e17f72b70d148
feat(holiday): 调整弹窗按日期显示国省道调整前值
1个文件已添加
5个文件已修改
230 ■■■■■ 已修改文件
HANDOFF.md 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleAdjustment.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentServiceTest.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/HolidayStat.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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)