支持项目编码访问单项目公示屏

需求:无需查询公示 Token,直接通过项目编码打开单项目公示页。

实现:规范化项目编码并按 code 查询,同时保留既有 Token 哈希查询;补充前后端及集成回归测试。
This commit is contained in:
2026-07-31 17:11:21 +08:00
parent 78df07e074
commit 9948c23bef
8 changed files with 108 additions and 8 deletions

View File

@@ -288,14 +288,27 @@ type displaySnapshotDTO struct {
LastUpdatedAt time.Time `json:"last_updated_at"`
}
func (s *Server) displaySnapshot(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(r.PathValue("token"))
if len(token) < 40 || len(token) > 128 {
writeError(w, &apiError{Status: http.StatusNotFound, Code: "DISPLAY_NOT_FOUND", Message: "公示屏绑定不存在"})
return
func normalizeDisplayProjectCode(value string) (string, bool) {
code := strings.ToUpper(strings.TrimSpace(value))
if !projectCodePattern.MatchString(code) {
return "", false
}
return code, true
}
func (s *Server) displaySnapshot(w http.ResponseWriter, r *http.Request) {
identifier := strings.TrimSpace(r.PathValue("token"))
var project model.Project
err := s.db.WithContext(r.Context()).Where("display_token_hash = ?", security.HashToken(token)).First(&project).Error
var err error
if code, ok := normalizeDisplayProjectCode(identifier); ok {
err = s.db.WithContext(r.Context()).Where("code = ?", code).First(&project).Error
} else {
if len(identifier) < 40 || len(identifier) > 128 {
writeError(w, &apiError{Status: http.StatusNotFound, Code: "DISPLAY_NOT_FOUND", Message: "公示屏绑定不存在"})
return
}
err = s.db.WithContext(r.Context()).Where("display_token_hash = ?", security.HashToken(identifier)).First(&project).Error
}
if err != nil {
writeError(w, mapNotFound(err, "DISPLAY_NOT_FOUND", "公示屏绑定不存在"))
return

View File

@@ -58,6 +58,26 @@ func TestPublicDisplayProjectViewOnlySerializesDisplayFields(t *testing.T) {
}
}
func TestNormalizeDisplayProjectCode(t *testing.T) {
tests := []struct {
input string
want string
ok bool
}{
{input: "raft", want: "RAFT", ok: true},
{input: " east-ride ", want: "EAST-RIDE", ok: true},
{input: "A", ok: false},
{input: "bad code", ok: false},
{input: strings.Repeat("a", 40), ok: false},
}
for _, test := range tests {
got, ok := normalizeDisplayProjectCode(test.input)
if got != test.want || ok != test.ok {
t.Fatalf("normalizeDisplayProjectCode(%q) = %q, %v; want %q, %v", test.input, got, ok, test.want, test.ok)
}
}
}
func TestPublicPhoneLookupIsDisabledInProduction(t *testing.T) {
server := &Server{config: config.Config{Environment: "production"}}
recorder := httptest.NewRecorder()

View File

@@ -158,6 +158,22 @@ func TestCurrentDayViewsIgnoreYesterdayRunningSessionPostgresIntegration(t *test
}
})
t.Run("display by project code", func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/display/"+strings.ToLower(project.Code)+"/snapshot", nil)
server.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
var response displaySnapshotDTO
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.Project.ID != project.ID {
t.Fatalf("project = %q, want %q", response.Project.ID, project.ID)
}
})
t.Run("admin project", func(t *testing.T) {
projection, _, _, _, _, err := server.adminProjectProjection(context.Background(), project)
if err != nil {