Add backend foundation and config-driven workbench

This commit is contained in:
2026-04-01 15:01:44 +08:00
parent 88b8f05f03
commit 94a1f0ba78
68 changed files with 10833 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
package service
import (
"context"
"encoding/json"
"net/http"
"cmr-backend/internal/apperr"
"cmr-backend/internal/store/postgres"
)
type ResultService struct {
store *postgres.Store
}
type SessionResultView struct {
Session EntrySessionSummary `json:"session"`
Result ResultSummaryPayload `json:"result"`
}
type ResultSummaryPayload struct {
Status string `json:"status"`
FinalDurationSec *int `json:"finalDurationSec,omitempty"`
FinalScore *int `json:"finalScore,omitempty"`
CompletedControls *int `json:"completedControls,omitempty"`
TotalControls *int `json:"totalControls,omitempty"`
DistanceMeters *float64 `json:"distanceMeters,omitempty"`
AverageSpeedKmh *float64 `json:"averageSpeedKmh,omitempty"`
MaxHeartRateBpm *int `json:"maxHeartRateBpm,omitempty"`
Summary map[string]any `json:"summary,omitempty"`
}
func NewResultService(store *postgres.Store) *ResultService {
return &ResultService{store: store}
}
func (s *ResultService) GetSessionResult(ctx context.Context, sessionPublicID, userID string) (*SessionResultView, error) {
record, err := s.store.GetSessionResultByPublicID(ctx, sessionPublicID)
if err != nil {
return nil, err
}
if record == nil {
return nil, apperr.New(http.StatusNotFound, "session_not_found", "session not found")
}
if userID != "" && record.UserID != userID {
return nil, apperr.New(http.StatusForbidden, "session_forbidden", "session does not belong to current user")
}
return buildSessionResultView(record), nil
}
func (s *ResultService) ListMyResults(ctx context.Context, userID string, limit int) ([]SessionResultView, error) {
if userID == "" {
return nil, apperr.New(http.StatusUnauthorized, "unauthorized", "user is required")
}
records, err := s.store.ListSessionResultsByUserID(ctx, userID, limit)
if err != nil {
return nil, err
}
results := make([]SessionResultView, 0, len(records))
for i := range records {
results = append(results, *buildSessionResultView(&records[i]))
}
return results, nil
}
func buildSessionResultView(record *postgres.SessionResultRecord) *SessionResultView {
view := &SessionResultView{
Session: buildEntrySessionSummary(&record.Session),
Result: ResultSummaryPayload{
Status: record.Status,
},
}
if record.Result != nil {
view.Result.Status = record.Result.ResultStatus
view.Result.FinalDurationSec = record.Result.FinalDurationSec
view.Result.FinalScore = record.Result.FinalScore
view.Result.CompletedControls = record.Result.CompletedControls
view.Result.TotalControls = record.Result.TotalControls
view.Result.DistanceMeters = record.Result.DistanceMeters
view.Result.AverageSpeedKmh = record.Result.AverageSpeedKmh
view.Result.MaxHeartRateBpm = record.Result.MaxHeartRateBpm
if record.Result.SummaryJSON != "" {
summary := map[string]any{}
if err := json.Unmarshal([]byte(record.Result.SummaryJSON), &summary); err == nil && len(summary) > 0 {
view.Result.Summary = summary
}
}
}
return view
}