feat(ECR-017): AskOperations 问答会话只读并 Closed
新增 admin.ask.read、GET /admin/ask/threads*(AskSessionView)与 admin-h5「问答」页;禁改消息/UGC/真支付。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// AskSessionView is ops read meta for one ask thread.
|
||||
type AskSessionView struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
ProfileID uuid.UUID `json:"profile_id"`
|
||||
Scene *string `json:"scene,omitempty"`
|
||||
MessageCount int `json:"message_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AskMessageView is a read-only message row for ops.
|
||||
type AskMessageView struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListAskSessions returns recent ask threads (optional user filter).
|
||||
func (r *AdminRepo) ListAskSessions(ctx context.Context, userID *uuid.UUID, limit, offset int) ([]AskSessionView, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT t.id, t.user_id, t.profile_id, t.scene, t.created_at, t.updated_at,
|
||||
(SELECT count(*)::int FROM ask_messages m
|
||||
WHERE m.thread_id=t.id AND m.deleted_at IS NULL) AS message_count
|
||||
FROM ask_threads t
|
||||
WHERE t.deleted_at IS NULL
|
||||
AND ($1::uuid IS NULL OR t.user_id=$1)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT $2 OFFSET $3`, userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []AskSessionView
|
||||
for rows.Next() {
|
||||
var s AskSessionView
|
||||
if err := rows.Scan(
|
||||
&s.ID, &s.UserID, &s.ProfileID, &s.Scene, &s.CreatedAt, &s.UpdatedAt, &s.MessageCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetAskSession loads one thread meta or ErrNoRows.
|
||||
func (r *AdminRepo) GetAskSession(ctx context.Context, threadID uuid.UUID) (*AskSessionView, error) {
|
||||
var s AskSessionView
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT t.id, t.user_id, t.profile_id, t.scene, t.created_at, t.updated_at,
|
||||
(SELECT count(*)::int FROM ask_messages m
|
||||
WHERE m.thread_id=t.id AND m.deleted_at IS NULL) AS message_count
|
||||
FROM ask_threads t
|
||||
WHERE t.id=$1 AND t.deleted_at IS NULL`, threadID,
|
||||
).Scan(&s.ID, &s.UserID, &s.ProfileID, &s.Scene, &s.CreatedAt, &s.UpdatedAt, &s.MessageCount)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// ListAskMessagesForAdmin returns messages oldest-first.
|
||||
func (r *AdminRepo) ListAskMessagesForAdmin(ctx context.Context, threadID uuid.UUID) ([]AskMessageView, error) {
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT id, role, content, created_at
|
||||
FROM ask_messages
|
||||
WHERE thread_id=$1 AND deleted_at IS NULL
|
||||
ORDER BY created_at ASC`, threadID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []AskMessageView
|
||||
for rows.Next() {
|
||||
var m AskMessageView
|
||||
if err := rows.Scan(&m.ID, &m.Role, &m.Content, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user