Files
CamTalk/backend/internal/store/user_scenario_repository.go
hhs d0e4bdaeec feat: 为 PostgreSQL store 层添加 trace 日志
为 4 个 PostgreSQL repository 添加 trace-aware 日志:
- session_pg.go: Save/Find/Update/Delete 操作日志
- user_pg.go: 用户 CRUD 和 refresh token 管理日志
- message_pg.go: 消息存储和查询日志
- user_scenario_repository.go: 自定义情景 CRUD 日志

日志策略:
- Error: 数据库操作失败
- Debug: 操作成功(避免 Info 级别噪音)
- NotFound (ErrNoRows) 不记录错误日志
2026-06-21 23:06:50 +08:00

277 lines
7.8 KiB
Go

package store
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/hhs/camtalk/internal/models"
"github.com/hhs/camtalk/internal/trace"
)
// UserScenarioRepository 用户自建情景仓储接口。
type UserScenarioRepository interface {
Create(ctx context.Context, scenario *models.UserScenario) error
FindByID(ctx context.Context, id string) (*models.UserScenario, error)
FindByIDAndUserID(ctx context.Context, id, userID string) (*models.UserScenario, error)
FindByUserID(ctx context.Context, userID string) ([]*models.UserScenario, error)
Update(ctx context.Context, scenario *models.UserScenario) error
Delete(ctx context.Context, id string) error
CountByUserID(ctx context.Context, userID string) (int, error)
}
// PostgresUserScenarioRepo PostgreSQL 实现。
type PostgresUserScenarioRepo struct {
pool *pgxpool.Pool
}
// NewPostgresUserScenarioRepo 创建 PostgreSQL 用户情景仓储。
func NewPostgresUserScenarioRepo(pool *pgxpool.Pool) UserScenarioRepository {
return &PostgresUserScenarioRepo{pool: pool}
}
// Create 创建用户情景。
func (r *PostgresUserScenarioRepo) Create(ctx context.Context, scenario *models.UserScenario) error {
log := trace.FromContext(ctx)
query := `
INSERT INTO user_scenarios (id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6, NULLIF($7, ''), $8, $9, $10)
RETURNING id, created_at, updated_at
`
now := time.Now()
scenario.CreatedAt = now
scenario.UpdatedAt = now
if scenario.ID == "" {
scenario.ID = uuid.New().String()
}
if scenario.Icon == "" {
scenario.Icon = "✨"
}
if scenario.Language == "" {
scenario.Language = "zh-CN"
}
err := r.pool.QueryRow(ctx, query,
scenario.ID,
scenario.UserID,
scenario.Name,
scenario.Icon,
scenario.Description,
scenario.Prompt,
scenario.Greeting,
scenario.Language,
scenario.CreatedAt,
scenario.UpdatedAt,
).Scan(&scenario.ID, &scenario.CreatedAt, &scenario.UpdatedAt)
if err != nil {
log.Errorw("create user scenario failed", "user_id", scenario.UserID, "name", scenario.Name, "error", err)
return fmt.Errorf("create user scenario: %w", err)
}
log.Debugw("user scenario created", "scenario_id", scenario.ID, "user_id", scenario.UserID, "name", scenario.Name)
return nil
}
// FindByID 根据 ID 查找情景。
func (r *PostgresUserScenarioRepo) FindByID(ctx context.Context, id string) (*models.UserScenario, error) {
log := trace.FromContext(ctx)
query := `
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
FROM user_scenarios
WHERE id = $1
`
var scenario models.UserScenario
err := r.pool.QueryRow(ctx, query, id).Scan(
&scenario.ID,
&scenario.UserID,
&scenario.Name,
&scenario.Icon,
&scenario.Description,
&scenario.Prompt,
&scenario.Greeting,
&scenario.Language,
&scenario.CreatedAt,
&scenario.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("user scenario not found: %s", id)
}
if err != nil {
log.Errorw("find user scenario failed", "scenario_id", id, "error", err)
return nil, fmt.Errorf("find user scenario: %w", err)
}
log.Debugw("user scenario found", "scenario_id", id)
return &scenario, nil
}
// FindByIDAndUserID 根据 ID 和用户 ID 查找情景(权限校验)。
func (r *PostgresUserScenarioRepo) FindByIDAndUserID(ctx context.Context, id, userID string) (*models.UserScenario, error) {
log := trace.FromContext(ctx)
query := `
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
FROM user_scenarios
WHERE id = $1 AND user_id = $2
`
var scenario models.UserScenario
err := r.pool.QueryRow(ctx, query, id, userID).Scan(
&scenario.ID,
&scenario.UserID,
&scenario.Name,
&scenario.Icon,
&scenario.Description,
&scenario.Prompt,
&scenario.Greeting,
&scenario.Language,
&scenario.CreatedAt,
&scenario.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("user scenario not found or no permission")
}
if err != nil {
log.Errorw("find user scenario by id and user failed", "scenario_id", id, "user_id", userID, "error", err)
return nil, fmt.Errorf("find user scenario: %w", err)
}
log.Debugw("user scenario found by id and user", "scenario_id", id, "user_id", userID)
return &scenario, nil
}
// FindByUserID 查找用户的所有情景。
func (r *PostgresUserScenarioRepo) FindByUserID(ctx context.Context, userID string) ([]*models.UserScenario, error) {
log := trace.FromContext(ctx)
query := `
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
FROM user_scenarios
WHERE user_id = $1
ORDER BY created_at DESC
`
rows, err := r.pool.Query(ctx, query, userID)
if err != nil {
log.Errorw("find user scenarios failed", "user_id", userID, "error", err)
return nil, fmt.Errorf("find user scenarios: %w", err)
}
defer rows.Close()
var scenarios []*models.UserScenario
for rows.Next() {
var s models.UserScenario
err := rows.Scan(
&s.ID,
&s.UserID,
&s.Name,
&s.Icon,
&s.Description,
&s.Prompt,
&s.Greeting,
&s.Language,
&s.CreatedAt,
&s.UpdatedAt,
)
if err != nil {
log.Errorw("scan user scenario row failed", "user_id", userID, "error", err)
return nil, fmt.Errorf("scan user scenario: %w", err)
}
scenarios = append(scenarios, &s)
}
if err = rows.Err(); err != nil {
log.Errorw("iterate user scenarios failed", "user_id", userID, "error", err)
return nil, fmt.Errorf("iterate user scenarios: %w", err)
}
log.Debugw("user scenarios found", "user_id", userID, "count", len(scenarios))
return scenarios, nil
}
// Update 更新用户情景。
func (r *PostgresUserScenarioRepo) Update(ctx context.Context, scenario *models.UserScenario) error {
log := trace.FromContext(ctx)
query := `
UPDATE user_scenarios
SET name = $1, icon = $2, description = $3, prompt = $4, greeting = $5, language = $6, updated_at = $7
WHERE id = $8 AND user_id = $9
RETURNING updated_at
`
scenario.UpdatedAt = time.Now()
err := r.pool.QueryRow(ctx, query,
scenario.Name,
scenario.Icon,
scenario.Description,
scenario.Prompt,
scenario.Greeting,
scenario.Language,
scenario.UpdatedAt,
scenario.ID,
scenario.UserID,
).Scan(&scenario.UpdatedAt)
if err == pgx.ErrNoRows {
return fmt.Errorf("user scenario not found or no permission")
}
if err != nil {
log.Errorw("update user scenario failed", "scenario_id", scenario.ID, "user_id", scenario.UserID, "error", err)
return fmt.Errorf("update user scenario: %w", err)
}
log.Debugw("user scenario updated", "scenario_id", scenario.ID, "user_id", scenario.UserID)
return nil
}
// Delete 删除用户情景。
func (r *PostgresUserScenarioRepo) Delete(ctx context.Context, id string) error {
log := trace.FromContext(ctx)
query := `DELETE FROM user_scenarios WHERE id = $1`
result, err := r.pool.Exec(ctx, query, id)
if err != nil {
log.Errorw("delete user scenario failed", "scenario_id", id, "error", err)
return fmt.Errorf("delete user scenario: %w", err)
}
if result.RowsAffected() == 0 {
return fmt.Errorf("user scenario not found")
}
log.Debugw("user scenario deleted", "scenario_id", id)
return nil
}
// CountByUserID 统计用户的情景数量。
func (r *PostgresUserScenarioRepo) CountByUserID(ctx context.Context, userID string) (int, error) {
log := trace.FromContext(ctx)
query := `SELECT COUNT(*) FROM user_scenarios WHERE user_id = $1`
var count int
err := r.pool.QueryRow(ctx, query, userID).Scan(&count)
if err != nil {
log.Errorw("count user scenarios failed", "user_id", userID, "error", err)
return 0, fmt.Errorf("count user scenarios: %w", err)
}
log.Debugw("user scenarios counted", "user_id", userID, "count", count)
return count, nil
}