feat: add exploration scale list, questions, and scoring

Seed communication-style scale, expose /api/v1/scales APIs, and wire
Explore/Scale H5 pages for the P1 探索测试 path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-02 16:29:31 +08:00
co-authored by Cursor
parent 14b836f53b
commit 15a9db374a
10 changed files with 478 additions and 5 deletions
+110
View File
@@ -0,0 +1,110 @@
package repository
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// ScaleListItem is a published 探索测试.
type ScaleListItem struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description"`
}
// ScaleQuestion is one item in a scale.
type ScaleQuestion struct {
ID uuid.UUID `json:"id"`
Sort int `json:"sort"`
Body json.RawMessage `json:"body"`
}
// ScaleDetail is scale + questions.
type ScaleDetail struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description"`
Questions []ScaleQuestion `json:"questions"`
}
// ScaleResultRow stored result.
type ScaleResultRow struct {
ID uuid.UUID `json:"id"`
ScaleSlug string `json:"scale_slug"`
Result json.RawMessage `json:"result"`
}
// ScaleRepo loads scales and results.
type ScaleRepo struct {
Pool *pgxpool.Pool
}
// ListPublished returns published scales.
func (r *ScaleRepo) ListPublished(ctx context.Context) ([]ScaleListItem, error) {
rows, err := r.Pool.Query(ctx, `
SELECT slug, title, description FROM scales
WHERE status='published' AND deleted_at IS NULL ORDER BY created_at`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ScaleListItem
for rows.Next() {
var it ScaleListItem
if err := rows.Scan(&it.Slug, &it.Title, &it.Description); err != nil {
return nil, err
}
out = append(out, it)
}
return out, rows.Err()
}
// GetBySlug loads scale with questions.
func (r *ScaleRepo) GetBySlug(ctx context.Context, slug string) (*ScaleDetail, error) {
d := &ScaleDetail{Slug: slug}
var scaleID uuid.UUID
err := r.Pool.QueryRow(ctx, `
SELECT id, title, description FROM scales
WHERE slug=$1 AND deleted_at IS NULL`, slug,
).Scan(&scaleID, &d.Title, &d.Description)
if err != nil {
return nil, err
}
rows, err := r.Pool.Query(ctx, `
SELECT id, sort, body FROM scale_questions
WHERE scale_id=$1 AND deleted_at IS NULL ORDER BY sort`, scaleID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var q ScaleQuestion
if err := rows.Scan(&q.ID, &q.Sort, &q.Body); err != nil {
return nil, err
}
d.Questions = append(d.Questions, q)
}
return d, rows.Err()
}
// SaveResult stores scoring output.
func (r *ScaleRepo) SaveResult(ctx context.Context, userID, scaleID, profileID uuid.UUID, answers, result json.RawMessage) (uuid.UUID, error) {
var id uuid.UUID
err := r.Pool.QueryRow(ctx, `
INSERT INTO scale_results(user_id, scale_id, profile_id, answers, result)
VALUES ($1,$2,$3,$4,$5) RETURNING id`,
userID, scaleID, profileID, answers, result,
).Scan(&id)
return id, err
}
// ScaleIDBySlug resolves id.
func (r *ScaleRepo) ScaleIDBySlug(ctx context.Context, slug string) (uuid.UUID, error) {
var id uuid.UUID
err := r.Pool.QueryRow(ctx, `
SELECT id FROM scales WHERE slug=$1 AND deleted_at IS NULL`, slug).Scan(&id)
return id, err
}