feat: 8 条功能 + 身份口径改为号码布为准(含批量退赛/移出、基础数据导入导出、登录有效期)
用户给出的 8 条功能 + 一条口径变更,全部带真机/真浏览器验证:
① 录入页姓名后直接显示号码布(原「号码」列保留)
② 录入页批次进度条:`第 2 批 已录 5/8 · 3 条有错`,录完变绿;已录判据复用
entryGuards.judgeEntryRow(与发布防呆同源,不会出现"进度条说录完、发布说缺 8 条")
⑦ 小项列表双击 → 跳到该小项的组别(编辑/删除按钮不误触)
③ 赛事开关 `standings.live_on_publish`(默认开=保持原行为):关闭后分批发布只上屏成绩,
名次/流水/榜单一起等该组录完再结算 —— 代理实读代码后纠正了我的前提:
奖牌榜读的是 results.rank(buildRanked)而非流水汇总,只延迟流水会出现
「有金牌但 0 分」。另提供 POST /api/results/settle 人工兜底 + GET /api/results/settlement 未结算清单
④ 组别「就地更新」PUT /api/events/{id}/categories/{categoryId}:有报名数据时也能改名/改年龄/
改性别/改权重(旧路径是整表替换,一改就被 409 拦下);改名不动报名/成绩/流水(真机逐字节比对);
权重真的变了才触发该组重算(容差 1e-9)
④辅 运动员列表多选+全选,POST /api/athletes/batch 一次请求完成整批停用/彻底删除(逐条回报,
dryRun 预览走同一段 SQL 但不提交、不写审计)——这才是绕开限流的正解
新增 报名项批量:POST /api/entries/batch(scratch/restore/remove)。退赛只动 registered、
跳过 dq/dns 并说明原因;移出是物理删除,必须显式清 score_ledger(该表 entry_id 是
ON DELETE SET NULL,不显式删会留下"人没了分还在"的孤儿流水,验证基线 orphan=0);
退赛同时清 results.rank(否则出现"0 分还挂着金牌")
⑤ GET /api/export/entries.xlsx 报名名单导出(13 列含批次,状态中文与录入页逐字一致)
⑥ 基础数据包:GET /api/export/basedata.xlsx + POST /api/export/basedata/import
(四张表、表头只有一份定义、表头行自动定位、列缺席≠写空、按编码 upsert、绝不删除、
行级中文错误带真实 Excel 行号、枚举走既有校验、新增单位一定是启用)
⑧ 登录有效期面板可配:系统设置新页签,1–720 小时,默认 12h → 24h;滑动续期 +
Cookie 同步刷新(只服务端续期是假的,浏览器侧的 Max-Age 到点仍会丢 Cookie)
身份口径:号码布为唯一标识、证件号退出匹配(号码布不同=同名的两个人,新建并单独报告);
同一份表重复证件号不再报错;两个不同的人共用证件号仍报行级错误(唯一索引保留)
其它对齐:分项成绩表草稿行不再消失(成绩列「分数未录入」、名次/积分 `—`,没成绩的行同占位);
大屏实时赛况单栏铺满(留白 370→78px)、字号上限 56→96px、≥7 行自动拆左右两块;
删除报名项/退赛/权重等改动都会重算并推榜单。
测试:服务端 13 包(连跑两遍)、前端 335 例、vue-tsc + build、Console 18 文件 —— 全绿。
文档:README、docs/ 四份(含 debug/使用说明 副本)、devdocs 05/08/04/13。
版本号 dev_0.1.5 → dev_0.1.6。
This commit is contained in:
1 parent
8dd599bd97
commit
631c8dd8e1
80 files changed
+12468
-519
No files matched your search
@@ -231,12 +231,12 @@ func TestImportBackfillsIDCardForExistingAthlete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 证件号命中但单位不一致 → 记为冲突,本轮不落库(人员与报名都不动)。
|
||||
// 号码布命中但单位不一致 → 记为冲突,本轮不落库(人员与报名都不动)。
|
||||
func TestImportCollectsConflictWithoutWriting(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234' WHERE id = ?`, ids.athleteA)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234', bib = 'A100' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,,,registered,\n"
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,A100,,registered,\n"
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import", body, "")
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
t.Fatalf("导入应 200,得到 %d %+v", status, env2.Error)
|
||||
@@ -285,8 +285,8 @@ func TestImportCollectsConflictWithoutWriting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同一文件里证件号重复出现 → 该行报错并跳过。
|
||||
func TestImportRejectsDuplicateIDCardInFile(t *testing.T) {
|
||||
// 同一文件里证件号重复出现不再报错:证件号是可选档案信息,不参与匹配也不拦截。
|
||||
func TestImportAcceptsDuplicateIDCardInFile(t *testing.T) {
|
||||
env, _, client := recorderWithSeed(t)
|
||||
|
||||
body := importHeader + "\n" +
|
||||
@@ -297,12 +297,14 @@ func TestImportRejectsDuplicateIDCardInFile(t *testing.T) {
|
||||
t.Fatalf("导入应 200,得到 %d %+v", status, env2.Error)
|
||||
}
|
||||
result := decodeData[map[string]any](t, env2)
|
||||
if result["created"] != float64(1) || result["skipped"] != float64(1) {
|
||||
t.Fatalf("首行应新建、次行应跳过: %+v", result)
|
||||
if result["skipped"] != float64(0) {
|
||||
t.Fatalf("同表重复证件号不应再拦截: %+v", result)
|
||||
}
|
||||
errs, _ := result["errors"].([]any)
|
||||
if len(errs) != 1 || !strings.Contains(errs[0].(string), "重复") {
|
||||
t.Fatalf("应报证件号重复,得到 %+v", errs)
|
||||
if errs, _ := result["errors"].([]any); len(errs) != 0 {
|
||||
t.Fatalf("不应有行级报错,得到 %+v", errs)
|
||||
}
|
||||
if result["created"] != float64(1) || result["updated"] != float64(1) {
|
||||
t.Fatalf("首行新建、次行按姓名+单位命中同一人并更新报名: %+v", result)
|
||||
}
|
||||
|
||||
var athletes int
|
||||
@@ -310,7 +312,118 @@ func TestImportRejectsDuplicateIDCardInFile(t *testing.T) {
|
||||
t.Fatalf("统计人员: %v", err)
|
||||
}
|
||||
if athletes != 1 {
|
||||
t.Fatalf("重复证件号只应建 1 个人员,得到 %d", athletes)
|
||||
t.Fatalf("同一个人只应建 1 个人员,得到 %d", athletes)
|
||||
}
|
||||
}
|
||||
|
||||
// 新人带的证件号已被库里另一个人占用 → 明确的中文行级错误,不静默改库、不静默跳过。
|
||||
func TestImportRejectsIDCardTakenByAnotherPerson(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234', bib = 'A100' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
// 另一个人(李四 / 号码布 B200)拿着已被张伟占用的证件号来建档。
|
||||
body := importHeader + "\nM100,男子,B,李四,110101199001011234,男,,B200,,registered,\n"
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import", body, "")
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
t.Fatalf("导入应 200(行级报错),得到 %d %+v", status, env2.Error)
|
||||
}
|
||||
result := decodeData[map[string]any](t, env2)
|
||||
if result["created"] != float64(0) || result["skipped"] != float64(1) {
|
||||
t.Fatalf("撞号的证件号应跳过该行: %+v", result)
|
||||
}
|
||||
errs, _ := result["errors"].([]any)
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("应报 1 条行级错误,得到 %+v", errs)
|
||||
}
|
||||
want := "第 2 行:证件号 110101199001011234 已被另一个人占用(姓名 张伟,号码布 A100)"
|
||||
if errs[0] != want {
|
||||
t.Fatalf("错误文案不对:\n 得到 %q\n 期望 %q", errs[0], want)
|
||||
}
|
||||
|
||||
// 库里的张伟一行没动,也没有多出一个人。
|
||||
var (
|
||||
name string
|
||||
bib string
|
||||
count int
|
||||
)
|
||||
if err := env.ev.QueryRow(`SELECT name, bib FROM athletes WHERE id = ?`, ids.athleteA).Scan(&name, &bib); err != nil {
|
||||
t.Fatalf("查询人员: %v", err)
|
||||
}
|
||||
if name != "张伟" || bib != "A100" {
|
||||
t.Fatalf("撞号不应改动库里已有人员,得到 name=%s bib=%s", name, bib)
|
||||
}
|
||||
if err := env.ev.QueryRow(`SELECT COUNT(*) FROM athletes`).Scan(&count); err != nil {
|
||||
t.Fatalf("统计人员: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("撞号不应新建人员,得到 %d 个", count)
|
||||
}
|
||||
}
|
||||
|
||||
// 号码布不同 → 按「同名的另一个人」新建档案,并在 sameNameNewPersons 里单独报告,不进冲突面板。
|
||||
func TestImportSameNameDifferentBibCreatesSecondPerson(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET bib = '1001' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
body := importHeader + "\nM100,男子,A,张伟,,男,,2001,,registered,\n"
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import", body, "")
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
t.Fatalf("导入应 200,得到 %d %+v", status, env2.Error)
|
||||
}
|
||||
result := decodeData[map[string]any](t, env2)
|
||||
if result["created"] != float64(1) || result["skipped"] != float64(0) {
|
||||
t.Fatalf("同名不同号码布应新建报名: %+v", result)
|
||||
}
|
||||
if conflicts, _ := result["conflicts"].([]any); len(conflicts) != 0 {
|
||||
t.Fatalf("同名不同号码布不进冲突面板: %+v", conflicts)
|
||||
}
|
||||
reported, _ := result["sameNameNewPersons"].([]any)
|
||||
if len(reported) != 1 {
|
||||
t.Fatalf("应报告 1 条同名新建,得到 %+v", reported)
|
||||
}
|
||||
item := reported[0].(map[string]any)
|
||||
if item["name"] != "张伟" || item["teamCode"] != "A" || item["newBib"] != "2001" || item["existingBib"] != "1001" {
|
||||
t.Fatalf("同名新建报告字段不对: %+v", item)
|
||||
}
|
||||
|
||||
var athletes int
|
||||
if err := env.ev.QueryRow(`SELECT COUNT(*) FROM athletes WHERE name = '张伟' AND team_id = ?`, ids.teamA).Scan(&athletes); err != nil {
|
||||
t.Fatalf("统计人员: %v", err)
|
||||
}
|
||||
if athletes != 2 {
|
||||
t.Fatalf("应新建第二个人,得到 %d 个", athletes)
|
||||
}
|
||||
// 原档案的号码布保持不动,新档案拿表里的号码布。
|
||||
var oldBib string
|
||||
if err := env.ev.QueryRow(`SELECT bib FROM athletes WHERE id = ?`, ids.athleteA).Scan(&oldBib); err != nil {
|
||||
t.Fatalf("查询原人员: %v", err)
|
||||
}
|
||||
if oldBib != "1001" {
|
||||
t.Fatalf("原档案号码布不应被改,得到 %s", oldBib)
|
||||
}
|
||||
}
|
||||
|
||||
// 姓名 + 单位命中多条候选:不能替用户挑人,进冲突面板等决策。
|
||||
func TestImportAmbiguousNameTeamGoesToConflictPanel(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
// 同单位两个「张伟」,都没有号码布,表里来第三个候选信息 → 歧义。
|
||||
env.exec(t, `INSERT INTO athletes (name, gender, team_id, active) VALUES ('张伟', '男', ?, 1)`, ids.teamA)
|
||||
|
||||
body := importHeader + "\nM100,男子,A,张伟,,男,,9001,,registered,\n"
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import", body, "")
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
t.Fatalf("导入应 200,得到 %d %+v", status, env2.Error)
|
||||
}
|
||||
result := decodeData[map[string]any](t, env2)
|
||||
if result["created"] != float64(0) || result["skipped"] != float64(0) {
|
||||
t.Fatalf("歧义行不落库也不跳过: %+v", result)
|
||||
}
|
||||
conflicts, _ := result["conflicts"].([]any)
|
||||
if len(conflicts) != 1 {
|
||||
t.Fatalf("应收集 1 条冲突,得到 %+v", conflicts)
|
||||
}
|
||||
if conflicts[0].(map[string]any)["athleteId"] != float64(ids.athleteA) {
|
||||
t.Fatalf("歧义冲突应以第一条候选为代表: %+v", conflicts[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,9 +473,9 @@ func TestImportToleratesMissingOptionalColumns(t *testing.T) {
|
||||
// keep-existing:保留库中人员字段,报名跟随库里已有的单位。
|
||||
func TestImportResolveKeepExisting(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234' WHERE id = ?`, ids.athleteA)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234', bib = 'A100' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,,,registered,\n"
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,A100,,registered,\n"
|
||||
|
||||
// 未给决策时,冲突原样返回。
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import/resolve", body, "[]")
|
||||
@@ -415,9 +528,9 @@ func TestImportResolveKeepExisting(t *testing.T) {
|
||||
// keep-new:用表里的字段更新人员,再按表里的单位建报名。
|
||||
func TestImportResolveKeepNew(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234' WHERE id = ?`, ids.athleteA)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234', bib = 'A100' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,,,registered,\n"
|
||||
body := importHeader + "\nM100,男子,B,张伟,110101199001011234,男,,A100,,registered,\n"
|
||||
decisions := `[{"idCard":"110101199001011234","action":"keep-new"}]`
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import/resolve", body, decisions)
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
@@ -618,44 +731,36 @@ func TestImportRejectsDuplicateBibInFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 证件号命中但号码布与库中不同 → 冲突,快照与差异字段都带 bib,本轮不落库。
|
||||
func TestImportConflictIncludesBibDiff(t *testing.T) {
|
||||
// 同名 + 号码布不同本应按「同名的另一个人」新建,但表里的证件号已被库里那个人占用时,
|
||||
// 唯一索引的底线更高:明确报出占用者,既不静默改库也不静默跳过。
|
||||
func TestImportSameNameDifferentBibButTakenIDCard(t *testing.T) {
|
||||
env, ids, client := recorderWithSeed(t)
|
||||
env.exec(t, `UPDATE athletes SET id_card = '110101199001011234', bib = 'A100' WHERE id = ?`, ids.athleteA)
|
||||
|
||||
body := importHeader + "\nM100,男子,A,张伟,110101199001011234,男,,A200,,registered,\n"
|
||||
status, env2 := env.postImport(t, client, "/api/entries/import", body, "")
|
||||
if status != http.StatusOK || !env2.OK {
|
||||
t.Fatalf("导入应 200,得到 %d %+v", status, env2.Error)
|
||||
t.Fatalf("导入应 200(行级报错),得到 %d %+v", status, env2.Error)
|
||||
}
|
||||
result := decodeData[map[string]any](t, env2)
|
||||
conflicts, _ := result["conflicts"].([]any)
|
||||
if len(conflicts) != 1 {
|
||||
t.Fatalf("应收集 1 条冲突,得到 %+v", conflicts)
|
||||
if result["skipped"] != float64(1) || result["created"] != float64(0) {
|
||||
t.Fatalf("撞号应跳过该行: %+v", result)
|
||||
}
|
||||
conflict := conflicts[0].(map[string]any)
|
||||
if conflict["bib"] != "A200" {
|
||||
t.Fatalf("冲突定位应带表里的号码布: %+v", conflict)
|
||||
errs, _ := result["errors"].([]any)
|
||||
want := "第 2 行:证件号 110101199001011234 已被另一个人占用(姓名 张伟,号码布 A100)"
|
||||
if len(errs) != 1 || errs[0] != want {
|
||||
t.Fatalf("错误文案不对:\n 得到 %+v\n 期望 %q", errs, want)
|
||||
}
|
||||
changed, _ := conflict["changedFields"].([]any)
|
||||
if len(changed) != 1 || changed[0] != "bib" {
|
||||
t.Fatalf("应只有 bib 变化,得到 %+v", changed)
|
||||
}
|
||||
existing := conflict["existing"].(map[string]any)
|
||||
if existing["bib"] != "A100" {
|
||||
t.Fatalf("existing 快照应带库中号码布: %+v", existing)
|
||||
}
|
||||
incoming := conflict["incoming"].(map[string]any)
|
||||
if incoming["bib"] != "A200" {
|
||||
t.Fatalf("incoming 快照应带表里号码布: %+v", incoming)
|
||||
if reported, _ := result["sameNameNewPersons"].([]any); len(reported) != 0 {
|
||||
t.Fatalf("新建被证件号唯一索引挡下时不应报告成同名新建: %+v", reported)
|
||||
}
|
||||
|
||||
var bib string
|
||||
if err := env.ev.QueryRow(`SELECT bib FROM athletes WHERE id = ?`, ids.athleteA).Scan(&bib); err != nil {
|
||||
t.Fatalf("查询人员号码布: %v", err)
|
||||
var athletes int
|
||||
if err := env.ev.QueryRow(`SELECT COUNT(*) FROM athletes`).Scan(&athletes); err != nil {
|
||||
t.Fatalf("统计人员: %v", err)
|
||||
}
|
||||
if bib != "A100" {
|
||||
t.Fatalf("冲突不应改库中号码布,得到 %s", bib)
|
||||
if athletes != 2 {
|
||||
t.Fatalf("不应新建人员,得到 %d 个", athletes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,3 +1371,94 @@ func TestUploadedImagePathRoundTripsThroughSettings(t *testing.T) {
|
||||
t.Fatalf("配置里的图片路径应可匿名访问,得到 %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 小项的 scored / recordEligible 漏传 = 建表默认值(migrations/event/0004 里都是 1),
|
||||
// 显式 false 才是 false;更新时漏传沿用库里的现值,与单位 / 大项的 active 同口径。
|
||||
// 与基础数据包导入侧(export_handlers.go 里新建小项默认 true/true)也一致。
|
||||
func TestEventBoolDefaultsFollowSchemaDefaults(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ids := env.seed(t)
|
||||
client := recorderClient(t, env)
|
||||
|
||||
boolField := func(t *testing.T, body envelope, field string) bool {
|
||||
t.Helper()
|
||||
got, ok := decodeData[map[string]any](t, body)[field].(bool)
|
||||
if !ok {
|
||||
t.Fatalf("响应里 %s 不是布尔值: %+v", field, body.Data)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
// 库里也真的按接口回报的值落着,不只是响应体好看。
|
||||
assertStoredFlags := func(t *testing.T, code string, want int) {
|
||||
t.Helper()
|
||||
var scored, recordEligible int
|
||||
if err := env.ev.QueryRow(`SELECT scored, record_eligible FROM events WHERE code = ?`, code).
|
||||
Scan(&scored, &recordEligible); err != nil {
|
||||
t.Fatalf("查询小项 %s 的开关: %v", code, err)
|
||||
}
|
||||
if scored != want || recordEligible != want {
|
||||
t.Fatalf("小项 %s 的开关应为 %d/%d,得到 %d/%d", code, want, want, scored, recordEligible)
|
||||
}
|
||||
}
|
||||
|
||||
base := map[string]any{"sportId": ids.sport, "code": "E-BOOL", "name": "开关缺省"}
|
||||
|
||||
status, body := env.do(client, http.MethodPost, "/api/events", base, true)
|
||||
if status != http.StatusOK || !body.OK {
|
||||
t.Fatalf("建小项应 200,得到 %d %+v", status, body.Error)
|
||||
}
|
||||
if !boolField(t, body, "scored") || !boolField(t, body, "recordEligible") {
|
||||
t.Fatalf("漏传开关应取建表默认值 true: %+v", body.Data)
|
||||
}
|
||||
assertStoredFlags(t, "E-BOOL", 1)
|
||||
eventID := int64(decodeData[map[string]any](t, body)["id"].(float64))
|
||||
|
||||
// 更新漏传沿用现值:库里是 true/true 时,只改名字不能把它们翻成 false。
|
||||
rename := map[string]any{"sportId": ids.sport, "code": "E-BOOL", "name": "开关缺省(改名一)"}
|
||||
status, body = env.do(client, http.MethodPut, fmt.Sprintf("/api/events/%d", eventID), rename, true)
|
||||
if status != http.StatusOK || !body.OK {
|
||||
t.Fatalf("改名应 200,得到 %d %+v", status, body.Error)
|
||||
}
|
||||
if !boolField(t, body, "scored") || !boolField(t, body, "recordEligible") {
|
||||
t.Fatalf("更新漏传应沿用库里的 true: %+v", body.Data)
|
||||
}
|
||||
|
||||
// 显式 false 才关得掉:接口不能无视调用方的明确意图。
|
||||
turnOff := map[string]any{
|
||||
"sportId": ids.sport, "code": "E-BOOL", "name": "开关缺省(改名二)",
|
||||
"scored": false, "recordEligible": false,
|
||||
}
|
||||
status, body = env.do(client, http.MethodPut, fmt.Sprintf("/api/events/%d", eventID), turnOff, true)
|
||||
if status != http.StatusOK || !body.OK {
|
||||
t.Fatalf("显式关开关应 200,得到 %d %+v", status, body.Error)
|
||||
}
|
||||
if boolField(t, body, "scored") || boolField(t, body, "recordEligible") {
|
||||
t.Fatalf("显式 false 应落库为 false: %+v", body.Data)
|
||||
}
|
||||
|
||||
// 反过来:库里是 false/false 时,更新漏传也不能把它打开。
|
||||
rename["name"] = "开关缺省(改名三)"
|
||||
status, body = env.do(client, http.MethodPut, fmt.Sprintf("/api/events/%d", eventID), rename, true)
|
||||
if status != http.StatusOK || !body.OK {
|
||||
t.Fatalf("再改名应 200,得到 %d %+v", status, body.Error)
|
||||
}
|
||||
if boolField(t, body, "scored") || boolField(t, body, "recordEligible") {
|
||||
t.Fatalf("更新漏传应沿用库里的 false: %+v", body.Data)
|
||||
}
|
||||
|
||||
// 新建时显式传 false 同样照办,缺省值不覆盖调用方的明确意图。
|
||||
explicitOff := map[string]any{
|
||||
"sportId": ids.sport, "code": "E-BOOL-OFF", "name": "开关显式关闭",
|
||||
"scored": false, "recordEligible": false,
|
||||
}
|
||||
status, body = env.do(client, http.MethodPost, "/api/events", explicitOff, true)
|
||||
if status != http.StatusOK || !body.OK {
|
||||
t.Fatalf("显式传 false 应 200,得到 %d %+v", status, body.Error)
|
||||
}
|
||||
if boolField(t, body, "scored") || boolField(t, body, "recordEligible") {
|
||||
t.Fatalf("显式 false 应落库为 false: %+v", body.Data)
|
||||
}
|
||||
assertStoredFlags(t, "E-BOOL-OFF", 0)
|
||||
assertStoredFlags(t, "E-BOOL", 0)
|
||||
}
|
||||
Reference in new issue
Block a user