3个文件已修改
31 ■■■■■ 已修改文件
HANDOFF.md 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/resources/application.yml.example 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -19,12 +19,15 @@
- 加固(已完成,编译/构建通过):后端 `listDirs` 子目录/文件列表各限 300(新增 `dirsTruncated`/`filesTruncated` 标记,`excelCount` 仍为真实总数);前端「从目录批量导入」每批限 60 个文件(超限提示分批);目录浏览弹窗显示截断提示。
- 待办:让崩溃机器用户在 Chrome 开 `chrome://crashes` 记下原因码(OOM / GPU / STATUS_ACCESS_VIOLATION 等),并确认崩溃时点的是侧边栏「数据导入」菜单还是页面上某个按钮;据此决定下一步。
- **重大进展(08-28 下午)**:
  - 用户确认:**崩溃时服务器后端日志无任何记录 → 请求根本没到后端** → 卡在**静态资源/网络层**(点菜单后浏览器现场下载数据导入页 chunk JS,还没到发 API 那一步)。接口/数据库彻底排除。
  - ~~请求根本没到后端~~(旧结论,已被推翻):用户后来确认 **chunk JS 4ms/7ms 正常,pending 的是 `/api/import/batches` 接口**(页面已进入,loadBatches+loadOverview 两个 batches 请求全部 pending);后端无日志=请求卡在 Tomcat 连接/线程层、未执行到 SQL。
  - 用户描述补充:点侧边栏「数据导入」菜单 → 无反应、停留主页 → 浏览器弹「应用程序无反应,是等待还是退出」= 渲染/浏览器进程 hang,非 crash。
  - 发布机器才是用户运行环境;本机 D:\trafficAudit 仅打包测试(17:39 后停止,日志正常)。
  - **本地打包形态压测(headless Chrome,20 轮菜单切换)**:导航 17~89ms、0 JS 错误、DOM 恒 949 节点无累积、堆内存 11~37MB 无泄漏、最长长任务 80ms → 应用 JS 层无死循环/大计算/泄漏。
  - **已加 Tomcat 访问日志**(`server.tomcat.basedir: .` + accesslog,jar 同级 `logs/access.*.log`,pattern 含 `%D` 耗时,静态资源也记录)——本地实测 chunk 请求 3ms。**重新打包发布后**,下次崩溃看 access log:chunk 请求是否到达、耗时多少 → 直接定位「服务器慢」还是「客户端没发出请求」。
  - 待用户:重新打包发布后,崩溃时看 ①服务器 `logs/access.*.log` ②客户端 F12 Network 中 `chunk-*.js` 请求状态(pending/stalled/耗时)。
  - **根因实锤(本机实验复现)**:Tomcat 9 NIO 下慢连接不占线程(slowloris 无效,已测),但**慢速 multipart 上传会占线程**——240 个慢上传占满线程池后,chunk 与 batches 请求全部 pending/超时(与用户现象一致)。**数据导入是同步长任务**(4 万行导入占 1 个线程 5 分钟,前端超时 600s),多用户同时导入/目录导入串行 → 200 线程耗尽 → 新请求(batches、静态文件)排队 pending → 浏览器等 →「无响应」弹窗。**只崩数据导入页**因为它进页即发 2 个 batches 请求且用户多在导入场景操作;其他页面在同样时刻也会 pending 只是未被注意。
  - **修复(commit 728b372,已验证)**:① `server.tomcat.threads.max: 500`(application.yml + example);② 数据导入页 `loadBatches`+`loadOverview` 合并为 `loadData`(进页/刷新/导入后只发 1 个 limit=200 请求,表格取前 50,概览复用同批数据)。**修复验证**:同样 240 慢上传压力下,batches 0.2s、chunk 0.1s 正常。
  - **遗留(建议下一步根治)**:导入异步化(@Async 独立线程池执行导入、前端轮询进度),彻底不占 Tomcat 请求线程;或 nginx 托管静态资源。对方 dev 机单用户也 pending,需看对方后端日志(可能 DB 连接/锁,或对方当时在跑导入),与本修复不冲突。
### 3. 待办不变
- `docs/打包部署说明.docx` 第六节「外置 application.yml 需包含完整配置」改为「属性级合并」后重新导出 docx(未做)。
traffic-audit-server/src/main/resources/application.yml.example
@@ -3,6 +3,8 @@
  port: 8090
  tomcat:
    basedir: .
    threads:
      max: 500
    accesslog:
      enabled: true
      directory: logs
traffic-audit-web/src/views/DataImport.vue
@@ -26,7 +26,7 @@
          <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
            <div class="card-head">本月导入进度({{ period || '未选择报表期' }})
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadOverview">刷新</el-button>
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadData">刷新</el-button>
            </div>
            <div v-if="!period" class="tip">请先在上方选择报表期</div>
            <div v-else class="overview-grid">
@@ -170,7 +170,7 @@
          <el-card shadow="never" style="margin-top:15px" class="batch-card">
            <div slot="header" class="card-head">
              导入记录({{ period || '全部报表期' }})
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadBatches">刷新</el-button>
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadData">刷新</el-button>
            </div>
            <el-table :data="batches" border size="small" v-loading="batchLoading" style="width:100%">
              <el-table-column prop="importType" label="导入类型" width="150">
@@ -394,7 +394,7 @@
      this.importType = this.moduleSel[key] || ''
      this.dirPath = (this.importType && localStorage.getItem('dirLast_' + this.importType)) || ''
    },
    onPeriodChange() { this.loadOverview(); this.loadBatches() },
    onPeriodChange() { this.loadData() },
    endpoint(t) {
      const map = {
        h2032: '/import/h2032',
@@ -655,13 +655,17 @@
        run(0)
      })
    },
    afterImport() { this.loadOverview(); this.loadBatches() },
    loadOverview() {
      if (!this.period) return
      api.get('/import/batches', { params: { period: this.period, limit: 200 } })
    afterImport() { this.loadData() },
    loadData() {
      if (this.batchLoading) return
      this.batchLoading = true
      const params = { limit: 200 }
      if (this.period) params.period = this.period
      api.get('/import/batches', { params })
        .then(res => {
          const map = {}
          const records = (res.data && res.data.records) || []
          this.batches = records.slice(0, 50)
          const map = {}
          records.forEach(r => {
            const key = this.batchKeyToType(r.importType)
            if (!key) return
@@ -673,6 +677,7 @@
          this.overview = map
        })
        .catch(() => {})
        .finally(() => { this.batchLoading = false })
    },
    batchKeyToType(k) {
      if (BATCH_TYPE_MAP[k]) return BATCH_TYPE_MAP[k]
@@ -724,8 +729,7 @@
    }
  },
  mounted() {
    this.loadBatches()
    this.loadOverview()
    this.loadData()
  }
}
</script>