# 独立核验报告 — 活动时间收敛为 5 分钟粒度(commit 707ea09ad) - **核验日期**:2026-10-24 - **核验人**:Agnes(software-qa-engineer,独立复核) - **提交对象**:`707ea09ad fix(activity):活动时间收敛为 5 分钟粒度(只到分,小程序+后台)` - **涉及文件**: - `cfc-frontend/pages/activity/create.vue`(+41/-6) - `cfc-web/src/views/admin/ActivityEdit.vue`(+40/-3) --- ## 逐条核验结果 ### ① 需求符合性 — PASS #### 小程序端 `normalizeTimeTo5Min` | 检查项 | 代码位置 | 结论 | |--------|---------|------| | 分钟向下取整到 5 的倍数 | `create.vue:369` `Math.floor(mi / 5) * 5` | ✓ | | 不跨小时进位(14:59→14:55,23:59→23:55) | `create.vue:370` `('0'+h).slice(-2)` 保留原小时 | ✓ | | 返回格式 `HH:MM`(无秒) | `create.vue:370` 返回 `'HH:MM'` | ✓ | **实测验证(node 1440 分钟全量扫描)**: ``` PASS: all 1440 minutes normalized correctly 14:07 -> 14:05 14:08 -> 14:05 14:59 -> 14:55 00:00 -> 00:00 23:59 -> 23:55 null -> null "" -> ``` - 零失败,所有分钟输出恒为 5 的倍数且小时不变。 #### 三个 `@change` 与 `fmtTime` 回填 | 处理函数 | 事件绑定位置 | 归一化接入 | |---------|-------------|-----------| | `onStartTimeChange` | `create.vue:24` `@change="onStartTimeChange"` | `create.vue:340` `this.timePart = this.normalizeTimeTo5Min(...)` ✓ | | `onEndTimeChange` | `create.vue:36` `@change="onEndTimeChange"` | `create.vue:349` `this.endTimePart = this.normalizeTimeTo5Min(...)` ✓ | | `onRegTimeChange` | `create.vue:48` `@change="onRegTimeChange"` | `create.vue:358` `this.regTimePart = this.normalizeTimeTo5Min(...)` ✓ | | `fmtTime` 回填 | `create.vue:590` | `create.vue:594` 调用 `this.normalizeTimeTo5Min(h + ':' + mi)` ✓ | #### `loadDetail` 三处归一化回填("显示即提交") | 字段 | loadDetail 调用位置 | 旧实现 | 新实现 | |------|-------------------|--------|--------| | `startTime` | `create.vue:304` | `self.form.startTime = sd.toISOString()` | `self.syncStartTime()` ✓ | | `endTime` | `create.vue:312` | `self.form.endTime = ed.toISOString()` | `self.syncEndTime()` ✓ | | `registrationDeadline` | `create.vue:320` | `self.form.registrationDeadline = rd.toISOString()` | `self.syncRegDeadline()` ✓ | #### 后台端 `format="yyyy-MM-dd HH:mm"` 隐秒列 - `ActivityEdit.vue:165` 开始时间 picker:`format="yyyy-MM-dd HH:mm"`(无 `ss`)✓ - `ActivityEdit.vue:177` 结束时间 picker:`format="yyyy-MM-dd HH:mm"`(无 `ss`)✓ - Element UI 2.15.14 datetime 面板中,无 `ss` 则秒列不渲染 ✓ #### `floorTo5Minutes` 双入参(Date/字符串) ```js // ActivityEdit.vue:372-379 floorTo5Minutes(value) { if (!value) return value const isDateObj = value instanceof Date const d = isDateObj ? new Date(value.getTime()) : new Date(value) if (isNaN(d.getTime())) return value // 非法输入原样返回 d.setMinutes(Math.floor(d.getMinutes() / 5) * 5, 0, 0) return isDateObj ? d : d.toISOString() // Date 入→Date 出;字符串入→ISO 串出 } ``` | 入参类型 | 行为 | 实测 | |---------|------|------| | `Date(14:07:30)` | 返回 Date,分钟→14:05,秒→0 | ✓ | | `'2026-10-25T14:07:00'` | 返回 ISO 串,分钟→14:05 | ✓ | | `null` | 原样返回 null | ✓ | | `''` | 原样返回 '' | ✓ | | `'not-a-date'` | 原样返回原串(不产生 Invalid Date)| ✓ | | `Date(00:00:00)` | 返回 Date,00:00:00 | ✓ | | `Date(23:59:59)` | 返回 Date,23:55:00 | ✓ | #### `loadDetail` 归一化回填 - `ActivityEdit.vue:416-417` `startTime: this.floorTo5Minutes(d.startTime)` ✓ - `ActivityEdit.vue:417` `endTime: this.floorTo5Minutes(d.endTime)` ✓ #### 全文 grep `selectableRange` / `timePickerOptions` 残留 ``` E:/cfc/cfc-frontend/pages/activity/create.vue: 无匹配 E:/cfc/cfc-web/src/views/admin/ActivityEdit.vue: 无匹配 ``` **结论:零残留** ✓ --- ### ② 实测函数行为 — PASS #### 测试 ①:1440 分钟逐一灌入 `normalizeTimeTo5Min` - **方法**:node 循环 h=0..23, mi=0..59,逐一调用,检查分钟恒为 5 倍数、小时不变 - **结果**:1440/1440 全部通过,零异常 - **边界**:00:00→00:00、23:59→23:55,均正确 #### 测试 ②:`floorTo5Minutes` 多场景 | 测试用例 | 入参 | 期望 | 实际 | 结果 | |---------|------|------|------|------| | Date 对象带秒 | `new Date(2026,9,25,14,7,30)` | Date,14:05:00 | 14:05:00 | ✓ | | Date 对象 | `new Date(2026,9,25,14,8,0)` | Date,14:05:00 | 14:05:00 | ✓ | | ISO 字符串 14:07 | `'2026-10-25T14:07:00'` | ISO 串,14:05 | 2026-10-25T06:05:00.000Z | ✓ | | ISO 字符串 14:08 | `'2026-10-25T14:08:00'` | ISO 串,14:05 | 2026-10-25T06:05:00.000Z | ✓ | | 00:00 | `new Date(0,0,0,0,0,0)` | Date,00:00:00 | 00:00:00 | ✓ | | 23:59 | `new Date(2026,9,25,23,59,59)` | Date,23:55:00 | 23:55:00 | ✓ | | null | `null` | null | null | ✓ | | undefined | `undefined` | undefined | undefined | ✓ | | 非法串 | `'not-a-date'` | 原串 `'not-a-date'` | `'not-a-date'` | ✓ | #### 测试 ③:模拟 `loadDetail` 回填 - **入参**:`startTime="2026-10-25T14:07:00"` - **流程**:`parseDate` → `fmtTime` → `normalizeTimeTo5Min` → `syncStartTime` - **期望**:`form.startTime = "2026-10-25T14:05:00"` - **实际**:`2026-10-25T14:05:00` ✓ --- ### ③ 契约未破坏 — PASS #### 小程序提交串格式 - `syncStartTime`: `this.datePart + 'T' + this.timePart + ':00'`(`create.vue:374`) - `syncEndTime`: 同上模式(`create.vue:379`) - `syncRegDeadline`: 同上模式(`create.vue:386`) - `timePart` 经 `normalizeTimeTo5Min` 后恒为 `HH:MM`(分钟 5 倍数,无秒) - **结果**:提交串恒为 `YYYY-MM-DDTHH:mm:00`,秒恒为 `00` ✓ #### 后端 `Activity.java:51` 解析兼容性 ```java // Activity.java:51 private Date startTime; // java.util.Date,无 @JsonFormat 注解 ``` - Jackson 默认序列化/反序列化 `java.util.Date` 使用 ISO 8601 格式(含秒) - 前端提交的 `YYYY-MM-DDTHH:mm:00` 是标准 ISO 8601,Jackson 可正常解析 ✓ - 无契约变更风险 ✓ --- ### ④ 提交卫生 — PASS #### 仅修改目标 2 个文件 ``` git show 707ea09ad --stat: cfc-frontend/pages/activity/create.vue | 41 ++++++++++++++++++++++++++------ cfc-web/src/views/admin/ActivityEdit.vue | 40 +++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 11 deletions(-) ``` #### 未提交文件核对(应保持未提交) | 文件 | 当前状态 | |------|---------| | `.gitignore` | Modified(未提交)✓ | | `cfc-backend/pom.xml` | Modified(未提交)✓ | | `cfc-web/AGENTS.md` | Modified(未提交)✓ | | `tools/xaf_agent_hub/tasks_backlog.json` | Modified(未提交)✓ | #### 无多余提交 / 未推送 - `git log --oneline -5`:`707ea09ad` 是当前 HEAD,前序为 `ef8d9d66c merge`,无多余提交 ✓ - `git branch -r --contains 707ea09ad`:无输出(未推送到任何远端分支)✓ - `origin/cfclub` 最新为 `ef8d9d66c`,不含 `707ea09ad` ✓ --- ### ⑤ BUG-1 / BUG-2 回归验证 — PASS(逻辑层面) > 说明:E2E 需在浏览器/开发工具内操作,以下基于代码逻辑推断 + 纯函数测试验证。 #### BUG-1:后台 datetime picker 只改日期不碰时间时,时间不重置为 00:00 - **根因消除**:`selectableRange` / `timePickerOptions` 两个 computed 已从 `ActivityEdit.vue` 完全移除(grep 零残留) - **`format="yyyy-MM-dd HH:mm"`** 仅影响面板展示列,不干预 `v-model` 值 - **`@change="onStartTimeChange"`** 触发 `floorTo5Minutes(val)`,传入的是当前 v-model Date 对象,小时分钟保留不变,仅秒置 0、分钟归 5 倍数 - **逻辑结论**:只改日期时,`onStartTimeChange` 不会将小时/分钟清零 → BUG-1 修复正确 ✓ #### BUG-2:手输合法时间失焦后正常写入且被归一化 - **手输 14:10** → `floorTo5Minutes` 收到 Date(14:10:00),`Math.floor(10/5)*5 = 10`,不变 → 写入 14:10 ✓ - **手输 14:07** → `floorTo5Minutes` 收到 Date(14:07:00),`Math.floor(7/5)*5 = 5`,归一化为 14:05 ✓ - **逻辑结论**:两种输入行为均符合预期 ✓ --- ## 已知遗留项(不阻塞放行) 1. **注释措辞**:源码注释写"就近收敛",实为**向下取整**(`Math.floor`)。语义轻微不准确但不影响行为正确性。 2. **后台分钟列全可点**:移除 `selectableRange` 后,Element UI datetime 面板分钟列 60 项均可点选,非 5 倍数需经 `@change` 兜底收敛。用户可短暂看到 14:07 再跳至 14:05,属已接受的设计代价。 3. **E2E 浏览器实测**:未能在真实 Element UI datetime picker 面板中完成 clickoutside + 手输联动测试(无 jsdom + vue-test-utils 环境),以上为逻辑层推断。 --- ## 最终结论 **PASS — 可以放行。** | 核验项 | 结果 | |--------|------| | 需求符合性(归一化逻辑/事件接入/loadDetail/无残留) | PASS | | 实测函数行为(1440 分钟全量/边界/无效输入/loadDetail 模拟) | PASS | | 契约未破坏(提交串格式/后端 Date 解析) | PASS | | 提交卫生(仅 2 文件/未推送/未提交文件未干扰) | PASS | | BUG-1/BUG-2 回归(逻辑层) | PASS | **建议**:后续在含 Element UI 2.15.14 + jest + @vue/test-utils 的环境中补做一次真实 E2E 回归,覆盖 BUG-1(日期切换保留时间)和 BUG-2(手输归一化)的端到端行为。