独立核验报告 — 活动时间收敛为 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
"" ->
三个 @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 ✓
- 逻辑结论:两种输入行为均符合预期 ✓
已知遗留项(不阻塞放行)
- 注释措辞:源码注释写"就近收敛",实为向下取整(
Math.floor)。语义轻微不准确但不影响行为正确性。
- 后台分钟列全可点:移除
selectableRange 后,Element UI datetime 面板分钟列 60 项均可点选,非 5 倍数需经 @change 兜底收敛。用户可短暂看到 14:07 再跳至 14:05,属已接受的设计代价。
- 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(手输归一化)的端到端行为。