feat: initialize Ballet Island 微信小程序 with basic structure and functionality

- Add project configuration for WeChat mini program.
- Create skills lock file for Taroify integration.
- Set up app configuration and styles.
- Implement main app component and index page with welcome message.
- Add health check service to verify backend readiness.
- Configure TypeScript settings and global types.
This commit is contained in:
yuxuanhui
2026-09-28 13:41:48 +08:00
commit 4b42b55928
32 changed files with 12054 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package httpapi
import (
"context"
"encoding/json"
"net/http"
"time"
)
// NewHandler returns health endpoints. ping must honor its context deadline.
func NewHandler(ping func(context.Context) error) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
writeStatus(w, http.StatusOK, "ok")
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := ping(ctx); err != nil {
// Do not expose database connection details through a public endpoint.
writeStatus(w, http.StatusServiceUnavailable, "unavailable")
return
}
writeStatus(w, http.StatusOK, "ok")
})
return mux
}
func writeStatus(w http.ResponseWriter, code int, status string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"status": status})
}
+67
View File
@@ -0,0 +1,67 @@
package httpapi
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestHealthRemainsLiveWithoutDatabase(t *testing.T) {
handler := NewHandler(func(context.Context) error {
t.Fatal("liveness must not query the database")
return nil
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if response.Code != http.StatusOK || response.Body.String() != "{\"status\":\"ok\"}\n" {
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
}
}
func TestReadinessReflectsDatabaseState(t *testing.T) {
for _, tc := range []struct {
name string
err error
code int
body string
}{
{"ready", nil, http.StatusOK, "{\"status\":\"ok\"}\n"},
{"unavailable", errors.New("private database details"), http.StatusServiceUnavailable, "{\"status\":\"unavailable\"}\n"},
} {
t.Run(tc.name, func(t *testing.T) {
called := false
handler := NewHandler(func(ctx context.Context) error {
called = true
deadline, ok := ctx.Deadline()
if !ok || time.Until(deadline) > 2*time.Second {
t.Fatal("database check must have a bounded deadline")
}
return tc.err
})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if !called || response.Code != tc.code || response.Body.String() != tc.body {
t.Fatalf("unexpected response: %d %s", response.Code, response.Body.String())
}
if !strings.HasPrefix(response.Header().Get("Content-Type"), "application/json") {
t.Fatal("health response must be JSON")
}
})
}
}
func TestReadinessPreservesRequestCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
handler := NewHandler(func(ctx context.Context) error { return ctx.Err() })
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/readyz", nil).WithContext(ctx)
handler.ServeHTTP(response, request)
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("got %d, want 503", response.Code)
}
}