feat: 数据导入页清空增强——支持按数据类型清空(后端 types 参数 + 弹窗多选),清空按钮移出“刷新”行改独立危险操作区防误触(含功能测试报告与工作日结)
4个文件已修改
1个文件已添加
386 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_导入清空按类型与按钮防误触_2026-09-10.md 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-10.md 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_µ¼ÈëÇå¿Õ°´ÀàÐÍÓë°´Å¥·ÀÎó´¥_2026-09-10.md
New file
@@ -0,0 +1,42 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šæ•°æ®å¯¼å…¥é¡µâ€œæ¸…ç©ºæ•°æ®â€å¢žå¼ºï¼ˆæŒ‰ç±»åž‹æ¸…ç©º + æŒ‰é’®ç§»å‡ºåˆ·æ–°è¡Œé˜²è¯¯è§¦ï¼‰ 2026-09-10
## åŠŸèƒ½è¯´æ˜Ž
- æ•°æ®å¯¼å…¥é¡µå³ä¾§â€œå¯¼å…¥è¿›åº¦â€ä¾§æ åœ¨åŽŸâ€œåˆ·æ–°â€æ—åŽ»æŽ‰â€œæ¸…ç©ºâ€ï¼Œæ”¹ä¸ºåœ¨ä¾§æ æ ‡é¢˜è¡Œä¸‹æ–¹æ–°å¢žç‹¬ç«‹å±é™©æ“ä½œåŒºï¼ˆæµ…çº¢åº• + è¾¹æ¡†ï¼‰ï¼Œå†…含 `el-button type="danger" plain` çš„“清空数据…”按钮与说明“按报表期清库,用于整月重导”,与“刷新”物理分开,降低误触。
- æ–°å¢žæ¸…空弹窗:显示当前报表期,选择“清空该报表期全部已导入数据”或“只清空指定类型”;选类型时用 `el-select multiple filterable`,按分组列出可选类型并标注“(已导入 N è¡Œ/未导入)”;底部红色警示语说明会同步删除审核结果、已审核标记与导入批次(投资项目主档保留)且不可撤销。
- åŽç«¯ `POST /api/import/clear` å¢žåŠ å¯é€‰å‚æ•° `types`(逗号分隔):为空 = æ¸…空该报表期全部类型(原行为不变);非空 = åªæ¸…空指定数据类型,并同步清理其关联的 audit_result / audit_run / import_batch。
## æ¶‰åŠæ”¹åЍ
- åŽç«¯ `DataImportService`:`clearPeriod(period, types)` é‡è½½ï¼›æ–°å¢žé™æ€æ˜ å°„ `CLEAR_TYPE_TO_OP`(类型→数据表操作键,同表归并去重)、`CLEAR_TYPE_TO_AUDIT`(类型→审核规则类型)、`CLEAR_TYPE_TO_BATCH`(类型→importType);新增 `clearTypeData(op, period)` åˆ†æ´¾ä¸Ž `deleteInvestmentMonthly(period, category)`(投资月度按项目类别过滤,仅删本期明细,投资主档保留)。返回值新增 `scope` ä¸Ž `types`,便于前端提示清空范围。
- åŽç«¯ `DataImportController`:`clearData` å¢žåŠ  `types` å‚数解析(空串/空项忽略)。
- å‰ç«¯ `DataImport.vue`:移除 `.side-head-btns` ä¸Žæ—§ `clearPeriodData()`;新增 `.side-danger` å±é™©æ“ä½œåŒºã€æ¸…空弹窗、`openClearDialog()/clearableGroups()/confirmClear()` ä¸Ž `clearDialog/clearScope/clearTypes` çŠ¶æ€ï¼›ç¡®è®¤åŽå†èµ°ä¸€æ¬¡ `$confirm` äºŒæ¬¡ç¡®è®¤ï¼ŒæˆåŠŸæç¤ºå¸¦åˆ é™¤è¡Œæ•°ã€‚
## æµ‹è¯•环境
- åŽç«¯ï¼štraffic-audit-server 8090(含本次改动;`start-dev.ps1` å¯åŠ¨ï¼ŒPID 21884,启动时间 2026-09-10 08:54:02,加载 `target/classes` æœ€æ–°ç¼–译产物)。
- å‰ç«¯ï¼štraffic-audit-web 8080 dev,`frontend.log` æœ€è¿‘一次 `Compiled successfully in 347ms`(08:53:29),热更新已生效。
- æ•°æ®åº“:本机 MySQL `localhost:3308/traffic_audit`,mysql CLI ç›´æŸ¥æ ¸å¯¹ï¼›ç™»å½•账号 `admin/123456`。
- æµ‹è¯•数据:一次性测试期 `2099-01 / 2099-02`,使用 `docs/货运需导入的数据/货运/2026å¹´1-7月报/道路货物运输月度生产情况(2026å¹´6月).xls`(636 è¡Œï¼‰ä¸Ž 2026-07 åŸŽå¸‚公交样本;真实月份仅做回归对比,不被写入或删除。
## ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | ç¼–译 | `mvn -o -q compile` | é€šè¿‡ | æ— æŠ¥é”™ | é€šè¿‡ |
| 2 | æ­£å‘-按类型清空 | `POST /import/clear?period=2099-01&types=h2032` | åªåˆ  h2032 åŠå…¶ H2032 å®¡æ ¸ç»“æžœ/批次 | dataRows=636、auditRows=31、auditRunRows=1、batchRows=1;DB æ ¸å¯¹ h2032=0、H2032 è§„则审核结果=0、audit_run(H2032)=0、批次(H2032)=0 | é€šè¿‡ |
| 3 | éš”离性-其它类型不受影响 | åŒä¸Šï¼Œæ¸… h2032 åŽæŸ¥ cityBus | cityBus æ•°æ®ä¸Žå®¡æ ¸/批次保留 | cityBus ä¸šåŠ¡ 142 è¡Œã€CITY_BUS å®¡æ ¸ 53 è¡Œã€audit_run(CITY_BUS)=1、批次(CITY_BUS)=1 å‡ä¿ç•™ | é€šè¿‡ |
| 4 | éš”离性-其它报表期不受影响 | åŒä¸Šï¼ŒæŸ¥ 2099-02 | 2099-02 æ•°æ®ä¸å—影响 | 2099-02 æ•°æ®å®Œå¥½ | é€šè¿‡ |
| 5 | è´Ÿå‘-非法类型 | `types=notAType` | æ‹’绝并提示 | HTTP 500,`清空失败: æœªæŒ‡å®šæœ‰æ•ˆçš„æ•°æ®ç±»åž‹` | é€šè¿‡ |
| 6 | è¾¹ç•Œ-有效+未知混合 | `types=cityBus,notAType` | å¿½ç•¥æœªçŸ¥ã€æŒ‰æœ‰æ•ˆç±»åž‹æ‰§è¡Œ | æˆåŠŸï¼Œè¿”å›ž `scope=types, types=[cityBus,notAType]`,删除 0 è¡Œï¼ˆè¯¥æœŸæ— æ•°æ®ï¼‰ | é€šè¿‡ |
| 7 | è¾¹ç•Œ-空类型列表 | ä¸ä¼  `types` | æŒ‰å…¨éƒ¨ç±»åž‹æ¸…空(原行为) | æˆåŠŸï¼Œ`scope=all` | é€šè¿‡ |
| 8 | æŠ•资按类别 | æ’å…¥ 2099-01 å®¢è¿ç«™åœº/物流园区测试行后分别清空 | åªåˆ å¯¹åº”类别,投资主档保留 | `types=investment` åªåˆ å®¢è¿ç«™åœºã€`types=investmentLogistics` åªåˆ ç‰©æµå›­åŒºï¼Œ`investment_project` ä¸»æ¡£ä¿ç•™ | é€šè¿‡ |
| 9 | UI-按钮位置 | æ‰“开数据导入页,展开“导入进度” | â€œåˆ·æ–°â€æ—æ— æ¸…空;清空按钮在独立红色危险区 | ä¸Žé¢„期一致(可访问性树可读到“清空数据…”按钮与提示文案) | é€šè¿‡ |
| 10 | UI-弹窗选项 | ç‚¹â€œæ¸…空数据…” | å¼¹çª—含报表期、全部/指定类型单选、类型下拉带导入状态 | é€‰ä¸­â€œé“路货物运输月度生产情况(已导入 636 è¡Œï¼‰â€ï¼Œé€‰é¡¹æ ‡ç­¾ä¸ŽçŠ¶æ€ä¸€è‡´ | é€šè¿‡ |
| 11 | UI-确认清空 | é€‰ä¸­ h2032 åŽç‚¹â€œç¡®è®¤æ¸…空” | äºŒæ¬¡ç¡®è®¤ â†’ æˆåŠŸæç¤ºå«è¡Œæ•°ã€è¿›åº¦å›žâ€œæœªå¯¼å…¥â€ | æç¤ºâ€œå·²æ¸…空 2099-01 æ•°æ®ï¼šä¸šåŠ¡æ•°æ® 636 è¡Œã€å®¡æ ¸ç»“æžœ 0 è¡Œã€å¯¼å…¥æ‰¹æ¬¡ 1 æ¡ï¼Œå¯ä»¥é‡æ–°å¯¼å…¥â€ï¼Œè¿›åº¦å›žâ€œæœªå¯¼å…¥â€ | é€šè¿‡ |
| 12 | å›žå½’-真实月份 | æ¸…空前后对比 | æ— å˜åŒ– | 2099-01 æ®‹ç•™=0;2026-09=636、2026-07=636、2025-07=664 æœªå˜ï¼›çœŸå®žæ‰¹æ¬¡æ€»æ•° 239 æœªå˜ | é€šè¿‡ |
## å‘现的问题与处置
- æ— åŠŸèƒ½æ€§ç¼ºé™·ã€‚æµ‹è¯•æœŸæ•°æ®åœ¨éªŒè¯ç»“æŸåŽå·²å…¨éƒ¨æ¸…é™¤ï¼Œæœªæ®‹ç•™ã€‚
- è¯´æ˜Žï¼ˆä¿ç•™ç‰¹æ€§ï¼‰ï¼šä¸€æ¬¡æ¸…空会删除该报表期“全部类型”的业务数据及审核结果/已审核标记/导入批次,弹窗已用红色文案提示不可撤销;只想清单一类型时使用“只清空指定类型”,未导入的类型仍可勾选但不产生删除。
- çŽ¯å¢ƒå¤‡æ³¨ï¼šæœ¬æœºåº“ `admin` å¯†ç ä¸º `123456`(`admin123` åœ¨æœ¬æœºä¸åŒ¹é…ï¼‰ã€‚
## ç»“论
- é€šè¿‡ã€‚“清空数据…”已从“刷新”旁移出并做视觉隔离,弹窗支持按整期或按类型清空,接口按范围精确删除业务数据、审核结果、已审核标记与导入批次,且对其它类型、其它报表期与真实月份零影响。
- å¾…用户在自己浏览器验收一次按钮位置与弹窗文案。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-10.md
@@ -19,5 +19,25 @@
- å¾…用户实际浏览器验收“清空”按钮(建议先确认所选报表期与备份);如需只清某一数据类型,可后续做“按类型清空”。
- æ²¿ç”¨æ˜¨æ—¥æœªåŠ¨å¾…åŠžï¼šæ±‡æ€»å¤§è¡¨ M3(网约车台账取数、货运页 FreightCalc è§„上/规下、城市客运/中口径合成页)与“9 æœˆ+无母版自动扩列”;8 æœˆä»ç¼º 4 ç±»æºæ•°æ®è¡¨è¡Œï¼ˆfreight_turnover_import、scale_split_transport、passenger_individual_monthly、网约车拆分输入)。
## ä¸‹åˆï¼ˆ2026-09-10)清空功能增强:支持按类型清空 + æ¸…空按钮移出“刷新”防误触
### å®Œæˆå†…容
- ç”¨æˆ·åé¦ˆâ€œæ¸…空放在刷新旁边怕误触”,且需要能只清某一数据类型:把清空从侧栏标题行(刷新旁)移除,改到标题行下方独立红色危险操作区(浅红底 + è¾¹æ¡†ï¼Œâ€œæ¸…空数据…”按钮 + æç¤ºâ€œæŒ‰æŠ¥è¡¨æœŸæ¸…库,用于整月重导”),与刷新物理隔开。
- æ–°å¢žæ¸…空弹窗:单选“清空全部 / åªæ¸…空指定类型”,指定类型用多选下拉并按分组列出、标签带“(已导入 N è¡Œ/未导入)”;底部红色警示说明会同步删审核结果、已审核标记、导入批次(投资项目主档保留)且不可撤销;确认前再走一次二次确认。
- åŽç«¯ `POST /api/import/clear` å¢žåŠ å¯é€‰ `types`(逗号分隔):空=整期全清(原行为不变),非空=只清指定类型,并同步清理关联 audit_result/audit_run/import_batch;返回值增加 `scope` ä¸Ž `types`。新增类型→表/审核类型/批次类型三张静态映射与 `clearTypeData` åˆ†æ´¾ï¼ŒæŠ•资月度按项目类别过滤(主档保留)。
### æµ‹è¯•结果摘要
- ç”¨ä¸€æ¬¡æ€§æµ‹è¯•期 2099-01/2099-02 éªŒè¯ï¼šæ¸… h2032 â†’ dataRows=636、auditRows=31、auditRunRows=1、batchRows=1,DB æ ¸å¯¹å››è¡¨å¯¹åº”记录全 0;同期的 cityBus ä¸šåŠ¡ 142 è¡Œã€CITY_BUS å®¡æ ¸ 53 è¡Œã€audit_run ä¸Žæ‰¹æ¬¡å‡ä¿ç•™ï¼›2099-02 æ•°æ®ä¸å—影响。
- è´Ÿå‘:`types=notAType` â†’ 500“未指定有效的数据类型”;有效+未知混合 â†’ å¿½ç•¥æœªçŸ¥æŒ‰æœ‰æ•ˆæ‰§è¡Œï¼›ä¸ä¼  types â†’ æ•´æœŸå…¨æ¸…。
- æŠ•资类别:`types=investment` åªåˆ å®¢è¿ç«™åœºã€`types=investmentLogistics` åªåˆ ç‰©æµå›­åŒºï¼ŒæŠ•资主档保留。
- UI å®žæµ‹ï¼ˆåº”用内浏览器,admin ç™»å½•):“刷新”旁已无清空;新危险区按钮与弹窗文案正常;选中“道路货物运输月度生产情况(已导入 636 è¡Œï¼‰â€ç¡®è®¤åŽæç¤ºâ€œå·²æ¸…空 2099-01 æ•°æ®ï¼šä¸šåŠ¡æ•°æ® 636 è¡Œã€å®¡æ ¸ç»“æžœ 0 è¡Œã€å¯¼å…¥æ‰¹æ¬¡ 1 æ¡ï¼Œå¯ä»¥é‡æ–°å¯¼å…¥â€ï¼Œè¿›åº¦å›žâ€œæœªå¯¼å…¥â€ã€‚
- å›žå½’:测试期无残留,真实月份 2026-09=636、2026-07=636、2025-07=664 ä¸ŽçœŸå®žæ‰¹æ¬¡æ€»æ•° 239 å‡æœªå˜ã€‚
- åŽç«¯ `mvn -o -q compile` é€šè¿‡ï¼ŒæœåŠ¡å·²é‡å¯ï¼ˆPID 21884,08:54 å¯åŠ¨ï¼‰ï¼›å‰ç«¯ dev `Compiled successfully`。
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_导入清空按类型与按钮防误触_2026-09-10.md。
### é—留问题与待办
- å¾…用户在自己浏览器验收新按钮位置与弹窗文案;如仍觉得危险区太显眼可再收进“更多操作”折叠。
- æ²¿ç”¨æœªåŠ¨å¾…åŠžï¼šæ±‡æ€»å¤§è¡¨ M3(网约车台账取数、货运页 FreightCalc è§„上/规下、城市客运/中口径合成页)与“9 æœˆ+无母版自动扩列”;8 æœˆä»ç¼º 4 ç±»æºæ•°æ®è¡¨è¡Œï¼ˆfreight_turnover_import、scale_split_transport、passenger_individual_monthly、网约车拆分输入)。
## é™„:提交
- æœ¬æ¬¡æ”¹åЍ 3 ä¸ªä»£ç æ–‡ä»¶ + æœ¬æŠ¥å‘Šä¸Žæ—¥ç»“,按规范先 `git fetch gitea/origin` å¯¹æ¯”合并,再 commit å¹¶åŒç«¯æŽ¨é€ï¼ˆgitea main、个人 gitee master)。
- æœ¬æ¬¡æ”¹åЍ 3 ä¸ªä»£ç æ–‡ä»¶ + åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šä¸Žæ—¥ç»“ï¼ŒæŒ‰è§„èŒƒå…ˆ `git fetch gitea/origin` å¯¹æ¯”合并,再 commit å¹¶åŒç«¯æŽ¨é€ï¼ˆgitea main、个人 gitee master)。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
@@ -443,14 +443,25 @@
        return Result.ok(result);
    }
    /** æ¸…空指定报表期全部已导入数据与审核结果(数据导入页"清空"按钮调用),便于整月重新导入 */
    /**
     * æ¸…空指定报表期已导入数据与审核结果(数据导入页"清空数据"入口调用),便于整月重新导入。
     * types ä¸ºç©ºè¡¨ç¤ºæ¸…空该报表期全部类型;否则只清空指定数据类型(逗号分隔,如 h2032,cityBus)。
     */
    @PostMapping("/clear")
    public Result<Map<String, Object>> clearData(@RequestParam("period") String period) {
    public Result<Map<String, Object>> clearData(@RequestParam("period") String period,
                                                 @RequestParam(value = "types", required = false) String types) {
        try {
            Map<String, Object> stats = importService.clearPeriod(period);
            List<String> typeList = new java.util.ArrayList<>();
            if (types != null) {
                for (String t : types.split(",")) {
                    String s = t.trim();
                    if (!s.isEmpty()) typeList.add(s);
                }
            }
            Map<String, Object> stats = importService.clearPeriod(period, typeList);
            return Result.ok(stats);
        } catch (Exception e) {
            log.error("clear period {} error", period, e);
            log.error("clear period {} types {} error", period, types, e);
            return Result.error("清空失败: " + e.getMessage());
        }
    }
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -1317,13 +1317,100 @@
        }
    }
    /** æ¸…空指定报表期全部已导入数据与审核结果(含导入批次、审核通过标记),用于整月重导;投资项目主档保留 */
    /** æ”¯æŒâ€œæŒ‰ç±»åž‹æ¸…空”的数据类型 -> æ•°æ®è¡¨æ“ä½œé”®ï¼ˆåŒè¡¨åŒç±»åˆ«å½’并,避免重复删除) */
    private static final Map<String, String> CLEAR_TYPE_TO_OP = new LinkedHashMap<>();
    /** æ•°æ®ç±»åž‹ -> å…³è”审核规则类型(同步清除审核结果与已审核标记) */
    private static final Map<String, String[]> CLEAR_TYPE_TO_AUDIT = new HashMap<>();
    /** æ•°æ®ç±»åž‹ -> å…³è”导入批次 importType(同步清除导入批次记录) */
    private static final Map<String, String[]> CLEAR_TYPE_TO_BATCH = new HashMap<>();
    static {
        CLEAR_TYPE_TO_OP.put("h2032", "H2032");
        CLEAR_TYPE_TO_OP.put("transportAuth", "TRANSPORT_AUTH");
        CLEAR_TYPE_TO_OP.put("trackMileage", "TRACK_MILEAGE");
        CLEAR_TYPE_TO_OP.put("scaleSplit", "SCALE_SPLIT");
        CLEAR_TYPE_TO_OP.put("freightTurnover", "FREIGHT_TURNOVER");
        CLEAR_TYPE_TO_OP.put("passengerMonthly", "H2031");
        CLEAR_TYPE_TO_OP.put("passengerAuth", "PASSENGER_AUTH");
        CLEAR_TYPE_TO_OP.put("passengerIndividual", "PASSENGER_INDIVIDUAL");
        CLEAR_TYPE_TO_OP.put("energyMonthly", "H204");
        CLEAR_TYPE_TO_OP.put("energyAuth", "ENERGY_AUTH");
        CLEAR_TYPE_TO_OP.put("investment", "INVEST_MONTHLY_PASSENGER");
        CLEAR_TYPE_TO_OP.put("investCityPassenger", "INVEST_MONTHLY_PASSENGER");
        CLEAR_TYPE_TO_OP.put("investmentLogistics", "INVEST_MONTHLY_LOGISTICS");
        CLEAR_TYPE_TO_OP.put("investCityLogistics", "INVEST_MONTHLY_LOGISTICS");
        CLEAR_TYPE_TO_OP.put("investmentSystem", "INVEST_SYSTEM");
        CLEAR_TYPE_TO_OP.put("cityBus", "CITY_BUS");
        CLEAR_TYPE_TO_OP.put("cityTaxi", "CITY_TAXI");
        CLEAR_TYPE_TO_OP.put("cityTaxiAuth", "CITY_TAXI_AUTH");
        CLEAR_TYPE_TO_OP.put("wycTotal", "WYC_TOTAL");
        CLEAR_TYPE_TO_OP.put("wycOrder", "WYC_ORDER");
        CLEAR_TYPE_TO_AUDIT.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("transportAuth", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("trackMileage", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("scaleSplit", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("freightTurnover", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("passengerMonthly", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("passengerAuth", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("passengerIndividual", new String[]{"H2031"});
        CLEAR_TYPE_TO_AUDIT.put("energyMonthly", new String[]{"H204"});
        CLEAR_TYPE_TO_AUDIT.put("energyAuth", new String[]{"H204"});
        CLEAR_TYPE_TO_AUDIT.put("investment", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investmentLogistics", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investCityPassenger", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investCityLogistics", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("investmentSystem", new String[]{"INVEST"});
        CLEAR_TYPE_TO_AUDIT.put("cityBus", new String[]{"CITY_BUS"});
        CLEAR_TYPE_TO_AUDIT.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_AUDIT.put("cityTaxiAuth", new String[]{"CITY_TAXI"});
        String[] investPassengerBatch = {"INVEST_客运站场", "INVEST_客运站场_CITY"};
        String[] investLogisticsBatch = {"INVEST_物流园区", "INVEST_物流园区_CITY"};
        CLEAR_TYPE_TO_BATCH.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_BATCH.put("transportAuth", new String[]{"TRANSPORT_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("trackMileage", new String[]{"TRACK_MILEAGE"});
        CLEAR_TYPE_TO_BATCH.put("scaleSplit", new String[]{"SCALE_SPLIT"});
        CLEAR_TYPE_TO_BATCH.put("freightTurnover", new String[]{"FREIGHT_TURNOVER"});
        CLEAR_TYPE_TO_BATCH.put("passengerMonthly", new String[]{"H2031"});
        CLEAR_TYPE_TO_BATCH.put("passengerAuth", new String[]{"PASSENGER_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("passengerIndividual", new String[]{"PASSENGER_INDIVIDUAL"});
        CLEAR_TYPE_TO_BATCH.put("energyMonthly", new String[]{"H204"});
        CLEAR_TYPE_TO_BATCH.put("energyAuth", new String[]{"ENERGY_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("investment", investPassengerBatch);
        CLEAR_TYPE_TO_BATCH.put("investCityPassenger", investPassengerBatch);
        CLEAR_TYPE_TO_BATCH.put("investmentLogistics", investLogisticsBatch);
        CLEAR_TYPE_TO_BATCH.put("investCityLogistics", investLogisticsBatch);
        CLEAR_TYPE_TO_BATCH.put("investmentSystem", new String[]{"INVEST_SYSTEM"});
        CLEAR_TYPE_TO_BATCH.put("cityBus", new String[]{"CITY_BUS"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxiAuth", new String[]{"CITY_TAXI_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("wycTotal", new String[]{"WYC_TOTAL"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER"});
    }
    /**
     * æ¸…空指定报表期已导入数据与审核结果,用于整月重导;投资项目主档保留。
     *
     * @param types éœ€æ¸…空的数据类型(前端类型键,如 h2032/cityBus);为空则清空该报表期全部类型
     */
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> clearPeriod(String period) {
    public Map<String, Object> clearPeriod(String period, List<String> types) {
        if (period == null || !period.matches("\\d{4}-\\d{2}")) {
            throw new IllegalArgumentException("报表期格式不正确,应为 yyyy-MM");
        }
        Set<String> selected = new LinkedHashSet<>();
        if (types != null) {
            for (String t : types) {
                if (t != null && !t.trim().isEmpty()) selected.add(t.trim());
            }
        }
        boolean clearAll = selected.isEmpty();
        int dataRows = 0;
        int auditRows = 0;
        int auditRunRows = 0;
        int batchRows = 0;
        if (clearAll) {
        dataRows += h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
            .eq(H2032EnterpriseMonthly::getReportPeriod, period));
        dataRows += transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
@@ -1358,23 +1445,145 @@
            .eq(WycTotalMonthly::getReportPeriod, period));
        dataRows += wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
            .eq(WycOrderMonthly::getReportPeriod, period));
        int auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
        auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
            .eq(AuditResult::getReportPeriod, period));
        int auditRunRows = auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
        auditRunRows = auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
            .eq(AuditRun::getReportPeriod, period));
        int batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
        batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
            .eq(ImportBatch::getReportPeriod, period));
        } else {
            Set<String> ops = new LinkedHashSet<>();
            Set<String> auditTypes = new LinkedHashSet<>();
            Set<String> batchTypes = new LinkedHashSet<>();
            for (String t : selected) {
                String op = CLEAR_TYPE_TO_OP.get(t);
                if (op == null) {
                    log.warn("清空数据:未知数据类型 {},已忽略", t);
                    continue;
                }
                ops.add(op);
                String[] at = CLEAR_TYPE_TO_AUDIT.get(t);
                if (at != null) java.util.Collections.addAll(auditTypes, at);
                String[] bt = CLEAR_TYPE_TO_BATCH.get(t);
                if (bt != null) java.util.Collections.addAll(batchTypes, bt);
            }
            if (ops.isEmpty()) {
                throw new IllegalArgumentException("未指定有效的数据类型");
            }
            for (String op : ops) {
                dataRows += clearTypeData(op, period);
            }
            if (!auditTypes.isEmpty()) {
                List<Long> ruleIds = new ArrayList<>();
                for (com.trafficaudit.rulemanage.entity.AuditRule rule : ruleMapper.selectList(null)) {
                    if (rule.getReportType() != null && auditTypes.contains(rule.getReportType())) {
                        ruleIds.add(rule.getId());
                    }
                }
                if (!ruleIds.isEmpty()) {
                    auditRows = auditResultMapper.delete(new LambdaQueryWrapper<AuditResult>()
                        .eq(AuditResult::getReportPeriod, period)
                        .in(AuditResult::getRuleId, ruleIds));
                }
                for (String rt : auditTypes) {
                    auditRunRows += auditRunMapper.delete(new LambdaQueryWrapper<AuditRun>()
                        .eq(AuditRun::getReportPeriod, period)
                        .eq(AuditRun::getReportType, rt));
                }
            }
            if (!batchTypes.isEmpty()) {
                batchRows = importBatchMapper.delete(new LambdaQueryWrapper<ImportBatch>()
                    .eq(ImportBatch::getReportPeriod, period)
                    .in(ImportBatch::getImportType, batchTypes));
            }
        }
        Map<String, Object> stats = new LinkedHashMap<>();
        stats.put("period", period);
        stats.put("scope", clearAll ? "all" : "types");
        if (!clearAll) stats.put("types", new ArrayList<>(selected));
        stats.put("dataRows", dataRows);
        stats.put("auditRows", auditRows);
        stats.put("auditRunRows", auditRunRows);
        stats.put("batchRows", batchRows);
        log.info("清空报表期 {} å®Œæˆï¼šä¸šåŠ¡æ•°æ® {} è¡Œã€å®¡æ ¸ç»“æžœ {} è¡Œã€å®¡æ ¸æ ‡è®° {} è¡Œã€å¯¼å…¥æ‰¹æ¬¡ {} è¡Œ",
            period, dataRows, auditRows, auditRunRows, batchRows);
        log.info("清空报表期 {}({})完成:业务数据 {} è¡Œã€å®¡æ ¸ç»“æžœ {} è¡Œã€å®¡æ ¸æ ‡è®° {} è¡Œã€å¯¼å…¥æ‰¹æ¬¡ {} è¡Œ",
            period, clearAll ? "全部类型" : String.join(",", selected), dataRows, auditRows, auditRunRows, batchRows);
        return stats;
    }
    /** æŒ‰â€œæ•°æ®è¡¨æ“ä½œé”®â€æ¸…空对应业务表(投资月度按项目类别过滤),返回删除行数 */
    private int clearTypeData(String op, String period) {
        switch (op) {
            case "H2032":
                return h2032Mapper.delete(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .eq(H2032EnterpriseMonthly::getReportPeriod, period));
            case "TRANSPORT_AUTH":
                return transportAuthMapper.delete(new LambdaQueryWrapper<TransportAuthVehicle>()
                    .eq(TransportAuthVehicle::getReportPeriod, period));
            case "TRACK_MILEAGE":
                return trackMileageMapper.delete(new LambdaQueryWrapper<VehicleTrackMileage>()
                    .eq(VehicleTrackMileage::getReportPeriod, period));
            case "SCALE_SPLIT":
                return scaleSplitMapper.delete(new LambdaQueryWrapper<ScaleSplitTransport>()
                    .eq(ScaleSplitTransport::getReportPeriod, period));
            case "FREIGHT_TURNOVER":
                return freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
                    .eq(FreightTurnoverImport::getReportPeriod, period));
            case "H2031":
                return passengerMapper.delete(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period));
            case "PASSENGER_AUTH":
                return passengerAuthMapper.delete(new LambdaQueryWrapper<PassengerAuthVehicle>()
                    .eq(PassengerAuthVehicle::getReportPeriod, period));
            case "PASSENGER_INDIVIDUAL":
                return passengerIndividualMapper.delete(new LambdaQueryWrapper<PassengerIndividualMonthly>()
                    .eq(PassengerIndividualMonthly::getReportPeriod, period));
            case "H204":
                return energyMapper.delete(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .eq(EnergyVehicleQuarterly::getReportPeriod, period));
            case "ENERGY_AUTH":
                return energyAuthMapper.delete(new LambdaQueryWrapper<EnergyAuthVehicle>()
                    .eq(EnergyAuthVehicle::getReportPeriod, period));
            case "INVEST_MONTHLY_PASSENGER":
                return deleteInvestmentMonthly(period, "客运站场");
            case "INVEST_MONTHLY_LOGISTICS":
                return deleteInvestmentMonthly(period, "物流园区");
            case "INVEST_SYSTEM":
                return investSystemMapper.delete(new LambdaQueryWrapper<InvestmentSystem>()
                    .eq(InvestmentSystem::getReportPeriod, period));
            case "CITY_BUS":
                return cityBusMapper.delete(new LambdaQueryWrapper<CityBusMonthly>()
                    .eq(CityBusMonthly::getReportPeriod, period));
            case "CITY_TAXI":
                return cityTaxiMapper.delete(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .eq(CityTaxiMonthly::getReportPeriod, period));
            case "CITY_TAXI_AUTH":
                return cityTaxiAuthMapper.delete(new LambdaQueryWrapper<CityTaxiAuth>()
                    .eq(CityTaxiAuth::getReportPeriod, period));
            case "WYC_TOTAL":
                return wycTotalMapper.delete(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, period));
            case "WYC_ORDER":
                return wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>()
                    .eq(WycOrderMonthly::getReportPeriod, period));
            default:
                log.warn("清空数据:未知数据表操作 {}", op);
                return 0;
        }
    }
    /** æŠ•资月度按项目类别清除(投资项目主档保留) */
    private int deleteInvestmentMonthly(String period, String category) {
        List<Long> projectIds = new ArrayList<>();
        for (InvestmentProject p : investProjectMapper.selectList(new LambdaQueryWrapper<InvestmentProject>()
                .eq(InvestmentProject::getCategory, category))) {
            projectIds.add(p.getId());
        }
        if (projectIds.isEmpty()) return 0;
        return investMonthlyMapper.delete(new LambdaQueryWrapper<InvestmentMonthly>()
            .eq(InvestmentMonthly::getReportPeriod, period)
            .in(InvestmentMonthly::getProjectId, projectIds));
    }
    /** æ•°æ®å˜åŒ–后删除指定报表类型指定报表期的审核结果(需重新执行审核) */
    private void deleteRuleResults(String reportType, String period) {
        List<Long> ruleIds = new ArrayList<>();
traffic-audit-web/src/views/DataImport.vue
@@ -103,11 +103,12 @@
              <div v-show="sideOpen" class="side-content">
                <div class="side-head">
                  <span>本月导入进度({{ period || '未选择报表期' }})</span>
                  <span class="side-head-btns">
                    <el-button type="text" icon="el-icon-refresh" @click="loadData">刷新</el-button>
                    <el-button type="text" icon="el-icon-delete" style="color:#F56C6C" :loading="clearing"
                               :disabled="!period || clearing || importing" @click="clearPeriodData">清空</el-button>
                  </span>
                </div>
                <div v-if="period" class="side-danger">
                  <el-button size="mini" type="danger" plain icon="el-icon-delete"
                             :disabled="clearing || importing" @click="openClearDialog">清空数据…</el-button>
                  <span class="side-danger-tip">按报表期清库,用于整月重导</span>
                </div>
                <div v-if="!period" class="tip">请先在上方选择报表期</div>
                <div v-else class="overview-grid">
@@ -123,6 +124,32 @@
            </div>
          </div>
        </div>
        <el-dialog title="清空数据" :visible.sync="clearDialog" width="560px" append-to-body :close-on-click-modal="false">
          <div class="clear-dlg">
            <div class="clear-period">报表期:<b>{{ period }}</b></div>
            <el-radio-group v-model="clearScope">
              <el-radio label="all">清空该报表期全部已导入数据</el-radio>
              <el-radio label="types">只清空指定类型</el-radio>
            </el-radio-group>
            <div v-if="clearScope === 'types'" class="clear-types">
              <el-select v-model="clearTypes" multiple filterable collapse-tags placeholder="选择要清空的数据类型" style="width:100%">
                <el-option-group v-for="g in clearableGroups()" :key="g.key" :label="g.name">
                  <el-option v-for="t in g.types" :key="t.value" :value="t.value"
                             :label="t.label + '(' + overviewState(t.value).text + ')'"></el-option>
                </el-option-group>
              </el-select>
              <div class="tip" style="margin-top:6px">可多选;未导入的类型没有数据可删。</div>
            </div>
            <div class="clear-warn">
              å°†åˆ é™¤æ‰€é€‰èŒƒå›´å†…该报表期的数据库数据,并同步删除对应审核结果、已审核标记与导入批次记录(投资项目主档保留)。此操作不可撤销,请确认已有备份或准备重新导入。
            </div>
          </div>
          <span slot="footer">
            <el-button @click="clearDialog = false">取消</el-button>
            <el-button type="danger" :loading="clearing"
                       :disabled="clearScope === 'types' && !clearTypes.length" @click="confirmClear">确认清空</el-button>
          </span>
        </el-dialog>
      </el-main>
    </el-container>
  </el-container>
@@ -163,6 +190,9 @@
      cityDirLoading: false,
      importing: false,
      clearing: false,
      clearDialog: false,
      clearScope: 'all',
      clearTypes: [],
      sideOpen: false,
      result: '',
      error: '',
@@ -525,19 +555,35 @@
        .catch(e => { if (!e || !e._toast) this.$message.error('导入记录加载失败,请刷新重试') })
        .finally(() => { this.batchLoading = false })
    },
    clearPeriodData() {
    openClearDialog() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.importing) { this.$message.warning('正在导入数据,请等待导入完成后再清空'); return }
      this.clearScope = 'all'
      this.clearTypes = []
      this.clearDialog = true
    },
    clearableGroups() {
      return this.groups.filter(g => !g.hidden)
    },
    confirmClear() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.clearing) return
      this.$confirm(
        '将清空 ' + this.period + ' æŠ¥è¡¨æœŸåœ¨æ•°æ®åº“中的全部已导入数据,同时删除该报表期的审核结果与导入批次记录(便于整月重新导入)。\n此操作不可撤销,是否继续?',
        '清空 ' + this.period + ' æ•°æ®',
        { type: 'warning', confirmButtonText: '确认清空', cancelButtonText: '取消' }
      ).then(() => {
        this.clearing = true
        api.post('/import/clear', null, { params: { period: this.period }, timeout: 600000 })
      if (this.clearScope === 'types' && !this.clearTypes.length) { this.$message.warning('请选择要清空的数据类型'); return }
      const scopeText = this.clearScope === 'all'
        ? '该报表期全部已导入数据'
        : '以下 ' + this.clearTypes.length + ' ä¸ªç±»åž‹ï¼š' + this.clearTypes.map(t => this.typeLabel(t)).join('、')
      this.$confirm('将清空 ' + this.period + ' çš„' + scopeText + ',并同步删除对应审核结果、已审核标记与导入批次记录。此操作不可撤销,是否继续?',
        '清空确认', { type: 'warning', confirmButtonText: '确认清空', cancelButtonText: '取消' })
          .then(() => {
            this.$message.success('已清空 ' + this.period + ' æŠ¥è¡¨æœŸçš„æ•°æ®ï¼Œå¯ä»¥é‡æ–°å¯¼å…¥')
          this.clearing = true
          const params = { period: this.period }
          if (this.clearScope === 'types') params.types = this.clearTypes.join(',')
          return api.post('/import/clear', null, { params, timeout: 600000 })
            .then(res => {
              const d = (res && res.data) || {}
              this.$message.success('已清空 ' + this.period + ' æ•°æ®ï¼šä¸šåŠ¡æ•°æ® ' + (d.dataRows || 0) +
                ' è¡Œã€å®¡æ ¸ç»“æžœ ' + (d.auditRows || 0) + ' è¡Œã€å¯¼å…¥æ‰¹æ¬¡ ' + (d.batchRows || 0) + ' æ¡ï¼Œå¯ä»¥é‡æ–°å¯¼å…¥')
              this.clearDialog = false
            this.loadData()
          })
          .catch(e => {
@@ -618,8 +664,12 @@
.import-side.side-open .side-toggle-text { writing-mode: horizontal-tb; letter-spacing: normal; margin-top: 0; }
.side-content { padding: 8px 12px 12px; max-height: calc(100vh - 220px); overflow-y: auto; overscroll-behavior: contain; }
.side-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-weight: 600; margin-bottom: 6px; }
.side-head-btns { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.side-head-btns .el-button + .el-button { margin-left: 0; }
.side-danger { display: flex; align-items: center; gap: 8px; margin: 4px 0 10px; padding: 6px 8px; background: #FEF0F0; border: 1px solid #FDE2E2; border-radius: 4px; }
.side-danger-tip { font-size: 12px; color: #F56C6C; line-height: 1.3; }
.clear-dlg { line-height: 1.7; }
.clear-period { margin-bottom: 6px; }
.clear-types { margin-top: 10px; }
.clear-warn { margin-top: 12px; padding: 8px 10px; background: #FEF0F0; border: 1px solid #FDE2E2; border-radius: 4px; color: #F56C6C; font-size: 12px; line-height: 1.6; }
.import-side .overview-grid { display: block; }
.import-side .ov-row { display: block; margin-bottom: 6px; }
.import-side .ov-group { width: auto; }