verify-activity-time-5min-commit.md 9.4 KB

独立核验报告 — 活动时间收敛为 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/字符串)

// 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 解析兼容性

// 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(手输归一化)的端到端行为。