## 一、修「改小项显示名称后,撤销发布按钮消失」 **根因不是改名字。** 我一开始怀疑「某处按 name 关联」,验下来那条路是干净的: 加载录入页报名行的查询、成绩查询、引擎读数据全是按 event_id / category_id / code 关联的 (并补了一条「小项名 + 组别名一起改」的回归用例,证明录入页状态 / 流水 / 榜单逐字节不变)。 真机制:**进入录入页时不记得上次录的是哪个小项**,兜底挑「第一个**待录入**(pending)的小项」。 而**刚发布完的小项状态已经是 published**,恰好被这条兜底排除 → 回去时右侧换成了**另一个小项**, 而「撤销发布」按钮只按「当前表里有没有已发布的行」显示,自然就不在。 ⇒ 现场看到的「左栏仍显示已发布、单位成绩仍有加分、但按钮没了」**三条本来不矛盾 —— 它们说的是两个不同的小项**。 **修法**:`pickInitialEventId()` 记住上次录入的小项(**按 id** 存 localStorage), 优先级 = URL 查询参数 → 上次录入的(**不排除已发布**)→ 第一个 pending → 列表第一个; 记忆失效(小项被删、隐私模式读不到存储)一律退回原兜底,不把人挡在录入页外。 **纯前端,零数据迁移。** 真机复现与复验(CDP 驱动真实浏览器): - 修前:「离开再回来 → 换成了别的小项、没有撤销发布按钮」; - 修后:「离开再回来 → 仍是那个小项、按钮在」。 ## 二、新增「清空本组 / 本批成绩」(危险操作) `POST /api/results/clear`(recorder 及以上 / CSRF / 幂等): - `heat` **省略 = 清整组**、数字 = 该批次、显式 `null` = 只清未分批的行(三种要能分清); - `confirm` 必须为 `true`(**服务端兜底**「没勾选不让提交」,不只是前端拦); - **不提供「清空整个小项」** —— 爆炸半径太大; - 语义 = **先撤销发布 + 再清内容**:成绩行整行归零 → **清整组时一并清掉「团体名次」** (否则会出现「清空了、榜上还有分」)→ 作废旧流水并按剩余已发布内容重写 → **小项状态对称退回**(只在原为 published 时降级,正好避免「左栏写着已发布却没有已发布成绩」) → 写审计 + 推榜。 界面:工具栏「清空本组」常驻;「清空本批」只在筛了批次时出现(没筛批次时它等于清整组,不重复给); 两粒都是危险色,示例数据模式 / 访客只读时禁用并说明原因。 **与「撤销发布」的区别**(界面上都写清了,很容易混): - **撤销发布**:成绩内容**保留**,只把分收回; - **清空**:成绩内容**一起清掉**,分也收回。 ## 三、顺带:按名字关联的同类扫描 扫了 13 处「按名字关联」的地方并逐条判定,结论: **数据关联一律按 id/code**;只有当「人写的名字要在表里对齐」时才按名字 (导入报名表里的组别名、Excel 工作表名、缺证件号时的同名兜底)—— 那几处是**有意为之**、报错也能照着改,**未改**。 ## 四、验证 - 全量自检两轮**全绿、退出码 0**:服务端 14 包 / 前端 **974 例** 0 fail / Console 96 例 0 fail。 - 新增用例:`event_rename_publish_test.go`(改名后状态/流水/榜单逐字节不变)、 `entry-initial-event.test.ts`(8 例)、`results_clear_test.go`、`entry-clear.test.ts`。 ## 版本 dev_0.5.11 → dev_0.5.12(无数据迁移)。
68 lines
3.6 KiB
TypeScript
68 lines
3.6 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
/**
|
|
* 示例数据模式下的写操作拦截(P2 A30)与「继续录入」的口径(P2 B09)。
|
|
*
|
|
* A30:发布 / 批次 / 团体名次早就按「示例数据不能写」停用了,只有「存草稿」与「撤销发布」
|
|
* 还会真的发请求,拿回一句网络错误——操作员会以为是自己手滑,而不是「这台机没连上服务端」。
|
|
* B09:注释与 07 文档都写「默认聚焦上次编辑的项目」,实现是取第一个 pending,
|
|
* 前端也从没持久化过录入位置,所以注释必须按实现写。
|
|
*
|
|
* 项目没有组件测试工具(无 @vue/test-utils / jsdom),界面部分按仓库既有做法读源码断言。
|
|
*/
|
|
|
|
const ENTRY_PAGE_SOURCE = readFileSync(new URL('../pages/EntryPage.vue', import.meta.url), 'utf8')
|
|
|
|
function section(source: string, start: string, end: string): string {
|
|
const from = source.indexOf(start)
|
|
const to = source.indexOf(end, from)
|
|
assert.ok(from >= 0 && to > from, `源码里找不到 ${start} … ${end} 这一段`)
|
|
return source.slice(from, to)
|
|
}
|
|
|
|
test('示例数据模式下「存草稿」不发请求,提示与发布同一口径', () => {
|
|
const saveDraft = section(ENTRY_PAGE_SOURCE, 'async function saveDraft(', 'function requestPublish(')
|
|
|
|
assert.match(saveDraft, /if \(data\.isMock\) \{/)
|
|
assert.match(saveDraft, /toast\.error\('示例数据模式不能保存草稿', '后端未就绪,草稿不会写入真实赛事'\)/)
|
|
// 早退必须发生在真正调用写接口之前。
|
|
assert.ok(
|
|
saveDraft.indexOf('data.isMock') < saveDraft.indexOf('await adminApi.saveDraft('),
|
|
'isMock 判断要在 saveDraft 调用之前',
|
|
)
|
|
})
|
|
|
|
test('示例数据模式下「撤销发布」不发请求,提示与发布同一口径', () => {
|
|
const confirmUnpublish = section(ENTRY_PAGE_SOURCE, 'async function confirmUnpublish(', '// ── 分批结算')
|
|
|
|
assert.match(confirmUnpublish, /if \(data\.isMock\) \{/)
|
|
assert.match(confirmUnpublish, /toast\.error\('示例数据模式不能撤销发布', '后端未就绪,撤销发布不会写入真实赛事'\)/)
|
|
assert.ok(
|
|
confirmUnpublish.indexOf('data.isMock') < confirmUnpublish.indexOf('await adminApi.unpublish('),
|
|
'isMock 判断要在 unpublish 调用之前',
|
|
)
|
|
})
|
|
|
|
test('两个按钮在示例数据模式下直接禁用,避免点了才被告知不能写', () => {
|
|
assert.match(ENTRY_PAGE_SOURCE, /:disabled="!canWrite \|\| saving \|\| allPublished \|\| data\.isMock"/)
|
|
assert.match(ENTRY_PAGE_SOURCE, /:disabled="!canWrite \|\| data\.isMock"/)
|
|
})
|
|
|
|
test('「继续录入」优先选上次录入的小项(按 id),兜底才是第一个 pending', () => {
|
|
const pickInitial = section(ENTRY_PAGE_SOURCE, '进入录入页时默认选中小项', 'onMounted(')
|
|
|
|
// 上次录入的小项按 id 记在本地:刚发布完的那个小项状态已是 published,
|
|
// 只按「第一个 pending」兜底会把它排除掉,回录入页就换成了别的小项(撤销发布按钮跟着消失)。
|
|
assert.match(pickInitial, /lastId: readLastEntryEventId\(entryMemoryStorage\(\)\)/)
|
|
assert.match(pickInitial, /events: data\.events/)
|
|
// 兜底行为没变:没有记忆时仍是「第一个未开始(pending)的项目,都没有就取列表第一个」。
|
|
assert.match(pickInitial, /第一个未开始(pending)的项目/)
|
|
assert.match(pickInitial, /pickInitialEventId/)
|
|
})
|
|
|
|
test('整份 EntryPage 里不再残留「默认聚焦上次编辑」这类与实现不符的说法', () => {
|
|
assert.doesNotMatch(ENTRY_PAGE_SOURCE, /默认聚焦上次编辑/)
|
|
})
|