package web import ( "net/http" "net/http/httptest" "net/url" "os" "strconv" "strings" "testing" ) // adminNotesBulk 必须走批量语句。 // // 旧实现是 `for _, id := range ids` 里逐条 Delete/SetNote/SaveVerdict:勾 50 个地点就是 // 50(清空是 100)次数据库往返,在 1C1G 的机器上体感很明显。这条守卫钉住「不许退回逐条循环」。 func TestAdminNotesBulkUsesBulkStatements(t *testing.T) { b, err := os.ReadFile(pkgFile(t, "handlers_admin.go")) if err != nil { t.Fatalf("读取 handlers_admin.go 失败: %v", err) } fn := funcBody(string(b), "func (s *Server) adminNotesBulk(") if fn == "" { t.Fatal("在 handlers_admin.go 里找不到 adminNotesBulk(写法变了,这条守卫就失效了)") } for _, banned := range []string{"s.notes.Delete(", "s.notes.SetNote(", "s.votes.SaveVerdict("} { if strings.Contains(fn, banned) { t.Errorf("adminNotesBulk 不该再逐条调用 %s —— 应改用对应的 *Bulk 方法", banned) } } for _, want := range []string{"s.notes.DeleteBulk(", "s.notes.SetNoteBulk(", "s.votes.SaveVerdictBulk("} { if !strings.Contains(fn, want) { t.Errorf("adminNotesBulk 未改用批量方法 %s", want) } } } // 批量勾选必须真的作用到**每一个**被勾的地点(不是只改第一条)。 func TestAdminNotesBulkAppliesToEveryCheckedPlace(t *testing.T) { f := newMovementFixture(t) uid := userFrom(f.ctx).ID pks := []int64{680001, 680002, 680003} for i, pk := range pks { if _, err := f.pg.ExecContext(f.ctx, `INSERT INTO places (dataset_id, src_pk, name, lat, lon) VALUES ($1,$2,'批量点',$3,$4)`, f.dsID, pk, 30.0+float64(i)*0.01, 120.0+float64(i)*0.01); err != nil { t.Fatalf("建地点失败: %v", err) } } t.Cleanup(func() { f.pg.ExecContext(f.ctx, `DELETE FROM place_notes WHERE user_id=$1`, uid) f.pg.ExecContext(f.ctx, `DELETE FROM place_votes WHERE user_id=$1`, uid) f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk = ANY($2::bigint[])`, f.dsID, pks) }) post := func(vals url.Values) { t.Helper() rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/admin/notes/bulk", strings.NewReader(vals.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") f.srv.adminNotesBulk(rec, req.WithContext(f.ctx)) if rec.Code != http.StatusSeeOther { t.Fatalf("批量操作应跳回页面: code=%d body=%s", rec.Code, rec.Body.String()) } } bulk := func(action string, extra ...string) url.Values { v := url.Values{"action": {action}} for i := 0; i+1 < len(extra); i += 2 { v.Set(extra[i], extra[i+1]) } for _, pk := range pks { v.Add("src_pk", strconv.FormatInt(pk, 10)) } return v } // 先把三个地点都写上别名 + 备注 for _, pk := range pks { if err := f.srv.notes.Save(f.ctx, uid, f.dsID, int(pk), "别名", "备注"); err != nil { t.Fatalf("预置备注失败: %v", err) } } post(bulk("verdict", "verdict", "1")) if n := f.scalar(t, `SELECT count(*) FROM place_votes WHERE user_id=$1 AND verdict=1`, uid); n != len(pks) { t.Fatalf("三个地点都该设上结论,实际 %d", n) } post(bulk("clear")) if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1 AND note <> ''`, uid); n != 0 { t.Fatalf("清空后不该还有手写备注,剩 %d 条", n) } if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1 AND alias='别名'`, uid); n != len(pks) { t.Fatalf("清空备注要保留别名,实际剩 %d 条", n) } post(bulk("verdict", "verdict", "0")) if n := f.scalar(t, `SELECT count(*) FROM place_votes WHERE user_id=$1`, uid); n != 0 { t.Fatalf("取消结论后不该还有记录,剩 %d 条", n) } post(bulk("delete")) if n := f.scalar(t, `SELECT count(*) FROM place_notes WHERE user_id=$1`, uid); n != 0 { t.Fatalf("删除后不该还有备注,剩 %d 条", n) } }